From cca4beb0ac2e8cf66c081c84fcdf6aa03bb09b6f Mon Sep 17 00:00:00 2001 From: Tim Biggs Date: Sun, 15 Nov 2026 00:39:06 +0000 Subject: [PATCH] fix(mobile): phone Claude Code follows Claude's update channel, latest by default Phone setup pinned Anthropic's apt stable repository, which trails latest by about two weeks, and the app's Update kept that source. Setup and Update now write the apt source for Claude Code's autoUpdatesChannel setting (user, then managed settings, later files win), latest unless it says stable, so existing phones move to latest on their next Update. Co-Authored-By: Claude Opus 5.5 --- .../src/main/assets/device-runtime-setup.sh | 30 +++++++++++-- .../device-runtime-script.test.ts | 42 ++++++++++++++++++- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/apps/mobile/modules/t3-device-runtime/android/src/main/assets/device-runtime-setup.sh b/apps/mobile/modules/t3-device-runtime/android/src/main/assets/device-runtime-setup.sh index e3307e47120f..f8ead04a7e3d 100644 --- a/apps/mobile/modules/t3-device-runtime/android/src/main/assets/device-runtime-setup.sh +++ b/apps/mobile/modules/t3-device-runtime/android/src/main/assets/device-runtime-setup.sh @@ -4,7 +4,8 @@ set -euo pipefail export DEBIAN_FRONTEND=noninteractive export PATH="$HOME/.local/bin:$PATH" NODE_CHANNEL=https://nodejs.org/dist/latest-v24.x -CLAUDE_LIST=/etc/apt/sources.list.d/claude-code.list +CLAUDE_LIST=${CLAUDE_LIST:-/etc/apt/sources.list.d/claude-code.list} +CLAUDE_MANAGED=${CLAUDE_MANAGED:-/etc/claude-code} # The fork whose releases carry a Linux arm64 server (FORK.md). RELEASE_REPO=tebiggs/t3code T3_LINK="$HOME/.local/bin/t3" @@ -55,20 +56,41 @@ step_node() { done } -# Anthropic's stable channel. The app checks the signing key's fingerprint and writes it -# first. One copy of the binary, updated only through apt. +# Anthropic's apt repository for the channel Claude Code's settings pick. The app checks the +# signing key's fingerprint and writes it first. One copy of the binary, updated only through apt. step_claude_code() { [ -s /etc/apt/keyrings/claude-code.asc ] || { echo "Claude Code's signing key is missing." >&2; exit 1; } - echo "deb [signed-by=/etc/apt/keyrings/claude-code.asc] https://downloads.claude.ai/claude-code/apt/stable stable main" > "$CLAUDE_LIST" + write_claude_list update_claude_repo apt_install claude-code } +# Moving to stable keeps a newer build until stable passes it. step_upgrade_claude_code() { + write_claude_list update_claude_repo apt-get install -y --only-upgrade claude-code } +write_claude_list() { + channel=$(claude_channel) + echo "deb [signed-by=/etc/apt/keyrings/claude-code.asc] https://downloads.claude.ai/claude-code/apt/$channel $channel main" > "$CLAUDE_LIST" + echo "Claude Code channel: $channel" +} + +# autoUpdatesChannel, latest by default as in Claude Code. Later files win, as in the server's +# ClaudeMaintenance.readUpdateChannel. +claude_channel() { + channel=latest + for file in "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" "$CLAUDE_MANAGED/managed-settings.json" \ + "$CLAUDE_MANAGED"/managed-settings.d/*.json; do + [ -f "$file" ] || continue + value=$(sed -n 's/.*"autoUpdatesChannel"[[:space:]]*:[[:space:]]*"\(latest\|stable\)".*/\1/p' "$file" | tail -n 1) + if [ -n "$value" ]; then channel=$value; fi + done + echo "$channel" +} + update_claude_repo() { apt-get update -o Dir::Etc::SourceList="$CLAUDE_LIST" -o Dir::Etc::SourceParts=- -o APT::Get::List-Cleanup=0 } diff --git a/apps/mobile/src/features/device-runtime/device-runtime-script.test.ts b/apps/mobile/src/features/device-runtime/device-runtime-script.test.ts index 8e0dcd2be89d..e1c5e512c44a 100644 --- a/apps/mobile/src/features/device-runtime/device-runtime-script.test.ts +++ b/apps/mobile/src/features/device-runtime/device-runtime-script.test.ts @@ -30,6 +30,7 @@ ln -sfn "$HOME/.t3/runtime/versions/$T3CODE_VERSION/t3" "$HOME/.local/bin/t3" `; interface Guest { + readonly root: string; readonly home: string; readonly log: string; run(step: string): string; @@ -64,6 +65,7 @@ esac `, ); writeExecutable(NodePath.join(bin, "node"), "#!/bin/sh\necho v24.22.1\n"); + writeExecutable(NodePath.join(bin, "apt-get"), `#!/bin/sh\necho "apt-get $*" >> "$STUB_LOG"\n`); writeExecutable( NodePath.join(bin, "claude"), `#!/bin/sh @@ -75,13 +77,21 @@ esac ); const versions = NodePath.join(home, ".t3/runtime/versions"); return { + root, home, log, run(step) { return NodeChildProcess.execFileSync("bash", ["-s", "--", step], { input: SCRIPT, encoding: "utf8", - env: { NODE_ENV: "test", HOME: home, PATH: `${bin}:/usr/bin:/bin`, STUB_LOG: log }, + env: { + NODE_ENV: "test", + HOME: home, + PATH: `${bin}:/usr/bin:/bin`, + STUB_LOG: log, + CLAUDE_LIST: NodePath.join(root, "claude-code.list"), + CLAUDE_MANAGED: NodePath.join(root, "claude-code"), + }, }); }, version(name) { @@ -132,6 +142,36 @@ describe("device-runtime-setup.sh", () => { expect(NodeFS.readFileSync(guest.log, "utf8")).toBe(""); }); + it.each([ + { settings: {}, channel: "latest" }, + { + settings: { ".claude/settings.json": '{ "autoUpdatesChannel": "stable" }' }, + channel: "stable", + }, + { + settings: { + ".claude/settings.json": '{"autoUpdatesChannel":"stable"}', + "claude-code/managed-settings.json": "{}", + "claude-code/managed-settings.d/10-channel.json": '{\n "autoUpdatesChannel": "latest"\n}', + }, + channel: "latest", + }, + ])("upgrades Claude Code from the $channel apt repository its settings pick", (row) => { + using guest = makeGuest(); + for (const [file, text] of Object.entries(row.settings)) { + const path = NodePath.join(file.startsWith(".claude") ? guest.home : guest.root, file); + NodeFS.mkdirSync(NodePath.dirname(path), { recursive: true }); + NodeFS.writeFileSync(path, text); + } + guest.run("upgrade-claude-code"); + expect(NodeFS.readFileSync(NodePath.join(guest.root, "claude-code.list"), "utf8")).toBe( + `deb [signed-by=/etc/apt/keyrings/claude-code.asc] https://downloads.claude.ai/claude-code/apt/${row.channel} ${row.channel} main\n`, + ); + expect(NodeFS.readFileSync(guest.log, "utf8")).toContain( + "apt-get install -y --only-upgrade claude-code", + ); + }); + it("reports versions and the Claude sign-in for the app", () => { using guest = makeGuest(); guest.link("0.1.3");