Skip to content

feat(mobile): run Claude Code on the Android phone itself through Termux - #9

Merged
tebiggs merged 14 commits into
mainfrom
t3/android-claude-code-setup
Oct 27, 2026
Merged

tebiggs merged 14 commits into
mainfrom
t3/android-claude-code-setup

Conversation

@tebiggs

@tebiggs tebiggs commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Claude Code can now run on an Android phone itself, as a normal T3 Code provider. T3 Code clients never run providers, so the phone runs its own T3 server inside Termux and the app connects to it over 127.0.0.1 like any other environment. "No project" threads, local-folder projects and folder browsing then work on the phone unchanged.

How it works

  • Add environment → Run agents on this phone (Android, 64-bit) opens a checklist. Every step is detected again whenever the screen gains focus or the app comes back from Termux, so the user can leave for Termux and the app can be killed without losing track:
    1. Install Termux (F-Droid).
    2. Let T3 Code use Termux: the RUN_COMMAND runtime permission, plus one pasted command that sets allow-external-apps = true (only Termux can set it). Permanently denied, Termux refusing, and Termux not answering each get their own message.
    3. Install Ubuntu, Claude Code and T3 Code. Termux opens and runs the installer, which takes a lock, records progress (the app shows "step N of 4", "stopped at step N" or "Finish install"), and only adds what is missing. A hand-made Ubuntu with Claude Code already signed in is kept as it is.
    4. Sign in to Claude: Termux opens claude auth login. Fallback: t3-phone shell, then claude auth login. Sign out is the reverse.
    5. Connect: starts t3 serve --host 127.0.0.1 as a Termux background task (output in ~/.t3-phone/server.log), mints a token with t3 pair inside Ubuntu, and pairs with expectedEnvironmentId set to whatever answered on the port, so a different listener can't be added by mistake.
  • Keep it running shows Termux's battery exemption and phone storage access, and links to the developer option that turns off Android's child-process limit (plus Samsung's "Never sleeping apps").
  • Afterwards: the app starts the server when it opens (at most once a minute, foreground only, off after an explicit Stop). Home shows a chip while it's down (tap to start, or open setup if starting failed). The environment page has Setup, Start/Stop and the auto-start switch. Removing the environment offers "Remove and stop server". Add project → Local folder gets Phone storage / Downloads / Documents / Ubuntu home shortcuts.

Pieces

  • apps/mobile/modules/t3-termux/: Expo module that sends Termux's RUN_COMMAND intent. Short background runs return stdout and the exit code through a PendingIntent; the server runs as a background task with no result; interactive steps get a terminal session, and the app brings Termux forward itself because Termux can't from the background on Android 10+. assets/t3-phone.sh is the one script behind every step (status, install, serve, stop, pair, login, logout, storage); it ships with the app, so it updates with the app, and install also saves it as t3-phone in Termux.
  • apps/mobile/src/features/phone/: checklist screen, step logic, server start/pair/auto-start, Home chip, environment section, folder shortcuts.
  • The server runs as proot's fake root with IS_SANDBOX=1, because Claude Code refuses full access as root otherwise. A separate Ubuntu user would add no isolation (it all runs as Termux's uid) and would make people who set Claude up by hand sign in again.
  • Fork releases now also build the Linux arm64 server (fork-release.yml, on GitHub's ubuntu-24.04-arm runner, like upstream's arm64 job). The phone installs the fork's newest release that has a server for its CPU, and falls back to upstream's nightly until one exists. A plain t3 serve can't update itself, so the phone's environment page has Update T3 Code: it installs the newest build and restarts the server.

No server changes. iOS is unchanged.

Testing

  • t3-phone-script.test.ts runs the real script under bash against a fake Termux (stubbed pkg, proot-distro, am, guest claude/t3): fresh status, a hand-made setup with Claude signed in, signed out, fresh install order and self-save, skipping finished parts, the install lock, a failed Ubuntu step, and pair parsing.
  • phone-setup.test.ts covers step order, the Termux opt-in states, token expiry, and install states (partial, running, killed midway, old failure).
  • t3-phone-script.test.ts also covers serve (supervises, logs, gives back its locks; no second server) and stop (a reused pid is left alone; success only once the supervisor is gone; failure keeps the lock), plus status during a running install, and update (picks the fork's newest server for the CPU from pretty or compact API JSON, falls back to upstream only when the fork has none, keeps the installed server when GitHub can't be reached, one update at a time).
  • Typecheck, lint, knip, ktlint and detekt pass.
  • Not yet run on a phone. Untested on hardware: the RUN_COMMAND result delivery, proot-distro 4.x vs 5.x on a real device, termux-open-url from inside Ubuntu, and t3 self-update under proot (a plain t3 serve offers no in-app update).

Mocks and a UX review (Claude Fable 5.1) shaped the flow before implementation: one detected checklist, and a failure state for every step. Six adversarial review rounds by GPT-6.1 Sol (Codex) followed; the later commits fix what they found, mostly around the server's lifecycle: a background terminal session that never started its process, Termux not coming forward on Android 14+, stop safety and confirmation, lock races, wake-lock cleanup, and the server updater.

Built with Claude Opus 5.5 in Claude Code (T3 Code).

🤖 Generated with Claude Code

tebiggs and others added 14 commits October 25, 2026 18:22
The Android app can now set the phone up as its own environment: Claude
Code and a T3 Code server run in Ubuntu (proot-distro) inside Termux, and
the app connects to that server over 127.0.0.1 like any other environment.
No-project threads, local-folder projects and folder browsing then work on
the phone unchanged.

Add environment → "Run agents on this phone" opens a checklist that
detects each step on every return from Termux: install Termux, allow
T3 Code to run commands there (RUN_COMMAND permission plus
allow-external-apps), install Ubuntu, Claude Code and T3 Code, sign in to
Claude, connect. A new t3-termux Expo module sends Termux's RUN_COMMAND
intent and reads results back; the bundled t3-phone script does the
work, records install progress behind a lock, and only adds what is
missing, so a hand-made Ubuntu with Claude Code is kept.

Afterwards the app starts the server when it opens (at most once a
minute, off after Stop), Home shows a chip while it's down, the
environment page has Start/Stop and the setup link, removing the
environment can also stop the server, and Add project → Local folder
gets phone storage shortcuts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s safe

Review fixes for running Claude Code on the phone:

- The server now runs as a Termux background task with its output in
  ~/.t3-phone/server.log. A terminal session started behind Termux never
  launched its process until Termux showed it.
- Install and sign-in sessions open Termux from the app itself, since
  Termux can't bring itself forward from the background on Android 10+.
- Stop signals only the process tree `t3-phone serve` started, checked by
  pid and start time, so a crashed server's reused pid is left alone.
- The installer lock is taken with an atomic mkdir before anything else,
  and only its owner removes it. Progress writes are atomic.
- Guest installs fail on a failed download (pipefail) and check that
  claude and t3 exist before recording success.
- Setup releases its wake lock when it ends unless the server holds one.
- Storage access is probed by reading shared storage, not ~/storage.
- Server probes time out after 3 s and share one start deadline; a failed
  start shows the server log's tail.
- "Remove and stop server" keeps the phone saved if stopping fails, and
  offers Remove anyway.
- The setup route renders a plain notice where the Termux bridge is
  missing (iOS deep links).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Second review round for running Claude Code on the phone:

- `t3-phone serve` stays alive as the server's supervisor instead of
  exec'ing away. It holds serve.lock and the wake lock while the server
  runs and gives both back however the server exits, so a failed start or
  a crash no longer leaves the wake lock held. A second serve while one is
  running or starting does nothing.
- Locks are symlinks whose target is the owner's pid:starttime, so taking
  a lock and recording its owner are one atomic step, and a reused pid
  can't pass for the owner.
- Stop signals the supervisor, which passes TERM to the server, and
  reports success only after the supervisor is gone (up to 10 s); a
  failed signal or a server that won't stop is an error and the lock stays.
- The app shares one pending start between Connect, the Home chip and
  auto-start, and auto-start checks it is still wanted right before
  launching. Remove and stop turns auto-start off first and restores it if
  the user keeps the phone after a failed stop.
- The environment page no longer claims that exiting Termux from its
  notification stops the server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Third review round:

- Stop and "Remove and stop server" wait for any start already handed to
  Termux before stopping. A dispatched start takes serve.lock only later,
  so stopping first missed it and the server came up after the phone was
  removed.
- Status returns as soon as it has reported a live installer's progress,
  without entering a half-installed Ubuntu that proot-distro may hold
  locked. A failed login there made the whole check fail mid-install.
- Locks are created with `ln -sT`, so a directory left at the lock path
  can't swallow the link and let two installers through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The installer released its lock only after switching back to T3 Code, and
status reports nothing but progress while the lock is held, so the app's
first check after setup could read "done" with no components and offer
Install again. The lock is now released before switching back, and a held
lock always reads as installing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… uses it

Fork releases now also build and publish the Linux arm64 server archive,
on GitHub's arm64 runner like upstream's job, since node-pty compiles from
source there. This is the server the Android app's phone setup runs in
Termux.

Phone setup installs the fork's newest release that has a server for the
phone's CPU, and falls back to upstream's nightly until one exists. A
plain `t3 serve` can't update itself, so the phone's environment page gets
Update T3 Code, which installs the newest build (`t3-phone update`) and
restarts the server on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the restart

Review fixes for Update T3 Code:

- The release lookup splits the API response into one field per line, so
  compact JSON can't make it pick an older release.
- A failed lookup stops the update instead of falling back to upstream's
  nightly, which would replace a fork build.
- `t3-phone update` holds update.lock, so a retry can't start a second
  installer while a slow one is still downloading. A timed-out update
  says it's still running instead of failing silently.
- Auto-start stays off while an update restarts the server, so it can't
  grab serve.lock between the stop and the start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Manual Start, the Home chip and Connect now wait out an update the same
way auto-start does, so none can take serve.lock between the update's
stop and its start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es the sheet

- The arm64 t3 binary links libatomic.so.1, which Ubuntu's container
  image leaves out, so the server failed its own install check ("error
  while loading shared libraries: libatomic.so.1"). Setup now installs
  libatomic1 with the other Ubuntu packages. Everything else the server
  and its native modules link is in the base image.
- "Run agents on this phone" in Add environment is a rounded card row like
  the managed-relay rows above it, and the sheet's scroll view gets bottom
  padding on Android so the last row clears the tab bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new thread's feed read the thread's detail through its route id before
the creation reached the server, skipping the delivery guard that the
thread selection already applies. When that read beat the creation, as it
does on a phone running its own server, the server answered not found, the
client cached the thread as deleted, and nothing updated until the app
restarted. The workflow-opening and guest-share reads now use the guarded
detail ref, which stays null until the creation is delivered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps the phone row's imports next to upstream dropping an unused one, and
passes ThreadFeed's new detailRef in upstream's new feed tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The restyled "Run agents on this phone" row dropped two things ListRow
gave it: TalkBack now reads the row's description again (64-bit
requirement or the saved phone's name), and Android presses show the
themed ripple.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phone setup installs upstream's nightly server when no recent fork release has
a Linux build for the phone, which would otherwise go unnoticed. `t3-phone
status` now reports which build is installed, telling them apart by the fork's
release repo inside the binary and caching the answer beside each version. The
setup screen and the phone's environment page show a warning with Update T3
Code as the fix.

Also states the reasons for the phone files' existing effects, which the
effect-budget lint rule from upstream now requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tebiggs
tebiggs merged commit 4b31963 into main Oct 27, 2026
12 checks passed
tebiggs added a commit that referenced this pull request Oct 28, 2026
Removes the Termux setup (#9): the t3-termux module, its script and the
host switch. "Run on this phone" now always sets up the in-app Ubuntu
runtime. Phone server settings saved by the Termux setup are dropped, and
connecting offers to remove a Termux-era environment.

Agents reach phone storage at /sdcard (and /storage): the runtime binds it
into Ubuntu, and setup's "Keep it running" section asks for All files access
(the storage permission before Android 11). The bind is always there, so a
grant takes effect without restarting the server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tebiggs added a commit that referenced this pull request Oct 29, 2026
…replacing Termux (#11)

* feat(mobile): prototype an on-device Ubuntu runtime without Termux

Phase 1 of running the T3 server and Claude Code on Android without Termux,
at the app's current targetSdk (36). A new Expo module, t3-device-runtime,
ships Termux's proot and loader as jniLibs (renamed lib*.so, NEEDED and
RUNPATH patched by scripts/vendor-proot.py) and extracts native libraries,
so Android lets the app execute them. proot runs every guest program through
the loader, which maps it from app storage instead of executing it.

The prototype screen (Run on this phone > Try it without Termux, or
t3code://device-runtime) downloads Ubuntu 26.04.1 base checked against
SHA256SUMS, runs uname, installs ca-certificates and the essentials with apt,
Node.js 24 LTS from nodejs.org checked against SHASUMS256, and Claude Code
from Anthropic's stable apt repository after checking the signing key's
fingerprint. It times each step and reports downloads and disk use.

x86_64 binaries are included so the same flow runs on emulators.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(mobile): run the phone's server without Termux

Phase 2 of the on-device runtime. Phone setup gets a "Without Termux"
switch; with it on, the checklist installs Ubuntu, Node.js, Claude Code and
the T3 server inside the app, signs in to Claude and connects the phone.

- The server runs under a foreground service (specialUse) with a partial
  wake lock, since Android freezes a backgrounded app's child processes.
- phone-server.ts now goes through a PhoneServerHost, so connect, auto-start,
  stop and update work the same for Termux and for the app's runtime.
  PhoneServerSettings records which host paired; settings saved before this
  have none and stay on Termux.
- The server comes from the fork's newest linux-arm64 release through
  t3.codes/install.sh, as in the Termux script. Update on the phone's
  environment page also upgrades claude-code from Anthropic's apt
  repository, then restarts the server.
- Claude sign-in runs `claude auth login` with a BROWSER that hands the URL
  to the app; the browser returns to Claude's localhost callback, or the user
  pastes the code it shows.
- Reinstall and Remove delete the runtime; the prototype screen is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mobile): on-device setup retries dropped connections

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mobile): stopping on-device commands ends proot and its guests

proot ignores SIGTERM, so Process.destroy() left it and the guest running.
Each guest command records proot's pid; stopping sends it SIGQUIT, which
makes proot kill its guests and exit, and SIGKILL after a grace period.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mobile): stop on-device commands through the guest's own process

proot ignores SIGQUIT too, and Android's destroyForcibly() only sends
SIGTERM. Stopping now signals the guest's main process, which proot passes
on; --kill-on-exit then ends the other guest processes and proot. The
server stops the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mobile): on-device setup keeps working in the background, one phone server at a time

- Install, update and Claude sign-in now run under the foreground service,
  with progress in its notification and a wake lock, so they keep going
  when the user leaves the app. Sign-in failed with EAI_AGAIN because the
  app, and the claude process, froze while the browser had the screen.
- Connecting a phone server replaces the phone's other one after asking:
  it stops that server and removes its environment. Remove and Reinstall
  also remove the runtime's environment, and Termux setup no longer takes
  the phone's pairing back from the runtime on its own.
- The phone's environment page shows whether Claude Code is signed in and
  warns when it isn't.
- resolv.conf retries with shorter timeouts, and a failed sign-in shows
  Claude's error instead of its whole output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(mobile): phone setup runs only inside the app, with phone storage

Removes the Termux setup (#9): the t3-termux module, its script and the
host switch. "Run on this phone" now always sets up the in-app Ubuntu
runtime. Phone server settings saved by the Termux setup are dropped, and
connecting offers to remove a Termux-era environment.

Agents reach phone storage at /sdcard (and /storage): the runtime binds it
into Ubuntu, and setup's "Keep it running" section asks for All files access
(the storage permission before Android 11). The bind is always there, so a
grant takes effect without restarting the server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(mobile): agents on the phone reach Android through termux-api commands

Agents in the on-device runtime lost Termux:API when the runtime left Termux.
While the phone's server runs, the app now serves a loopback HTTP bridge that
only the guest can authenticate to, through a token in /run/t3-android/endpoint,
and installs Termux:API-compatible commands that call it: termux-open-url,
xdg-open, termux-clipboard-get and -set, termux-notification and -remove,
termux-toast, termux-share, termux-vibrate and termux-battery-status.

Android blocks activity starts from the background, so links and shares asked
for while T3 Code isn't on screen arrive as a notification to tap. Clipboard
reads fail with a clear error in the background on Android 10+.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(mobile): bridge script test passes typecheck

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mobile): the phone's server survives a failed first start; /sdcard works after a late grant

On a fresh install the server's first start failed about every other time:
it publishes its environment id with a hard link from a temp directory, proot
emulates the link with a file in that directory, and the directory's removal
then fails with ENOTEMPTY depending on listing order. The id is in place by
then, so the runtime starts the server once more when it exits with an error
in its first 30 seconds and nobody asked it to stop.

proot drops a bind whose source it can't read when it starts, which
/storage/emulated/0 is until All files access is granted. The runtime now
binds /storage only and makes /sdcard a symlink into it, so a grant after the
server starts works without a restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(mobile): Agent access settings for the phone's termux commands

Each category of phone data or action (location, camera, microphone,
contacts, text messages, call log, phone, notifications) is Ask each time,
Always allow or Never, and asks Android for the permission it needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(mobile): agents on the phone reach more of Android, with per-category access

Agents on the phone could only open links, use the clipboard, post
notifications, share text, vibrate and read the battery. They had no way to
open files, use the camera, microphone or location, or read contacts, SMS or
the call log, and nothing let the user decide which of those an agent may use.

The bridge now answers 22 more Termux:API commands with Termux's flags and
JSON shapes: termux-open (files go out through a content URI with a read
grant), download, media-scan, torch, tts-speak, tts-engines, volume, sensor,
wallpaper, wifi-connectioninfo, camera-info, camera-photo, microphone-record,
location, contact-list, sms-list, sms-send, call-log, telephony-call,
telephony-deviceinfo, telephony-cellinfo and notification-list. xdg-open now
behaves as termux-open does.

Commands that reach personal data or sensitive hardware belong to eight
categories (location, camera, microphone, contacts, sms, call-log, phone and
notifications). Each one is set to ask, allow or deny, and ask is the default.
The policy is enforced in Claude Code's managed settings in the guest, which
are written when the server starts and whenever a policy changes, with other
keys left in place. Ask rules prompt even in bypassPermissions mode, so the
user approves those commands from the thread. The bridge also refuses denied
commands, and it refuses any command whose Android permission hasn't been
granted, saying where to allow it.

Android allows camera, microphone and location only while T3 Code is on
screen, so those commands fail with a clear error at other times.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: agent access uses the app's labels; restricted settings for sideloaded installs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mobile): a refused termux command names the setting as the app shows it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(mobile): Claude sessions on the phone know what Android lets them do

The runtime installs a short guide as Claude Code's managed CLAUDE.md in the
phone's Ubuntu: phone storage's limits, which network tools Android blocks,
the termux-* commands, which ones need the user's say in Agent access, and
what stops working while T3 Code is in the background. It tells agents to
send the user to the setting rather than work around a refusal. Only the
phone's Ubuntu has it, and the user's own CLAUDE.md stays theirs.

termux-wifi-connectioninfo also says on stderr that the network name is
hidden until T3 Code has Location, so any agent can explain it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mobile): agents' phone commands ask Android for the permission, and Agent access is in Settings

Approving a termux-location run in the thread wasn't enough: Android had never given
T3 Code Location, so the command failed at once. Now the bridge shows Android's prompt
over T3 Code and waits for the answer; in the background it posts a notification that
opens the prompt. Agent access moves to Settings and the phone's environment page, and
the guide and errors name that path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(mobile): follow main's renames in the device runtime screens

main removed SettingsChoiceRow and renamed the react-effect-budget lint rule to
no-unreasoned-state-sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(mobile): mock the fork's phone row in the Add environment test

Upstream's new test mocks only what upstream's screen imports. The fork's phone row
adds RowPressable (which now loads gesture-handler), uniwind, the device runtime and
the phone settings, so the suite failed to load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mobile): review fixes for the phone's agent bridge and runtime

- The bridge caps connections, header count and request time, and checks the token
  before reading the body, so another app on the phone can't exhaust it.
- Stopping a guest right after it starts waits for its pids instead of signalling none.
- termux-wifi-connectioninfo hides the network name and BSSID unless Location is
  Always allow; they locate the phone, and the command never asks.
- Setting Microphone to Never stops a recording in progress.
- expo-image-picker's microphonePermission: false blocked RECORD_AUDIO app-wide, so
  termux-microphone-record never worked; an Android-only plugin lifts that block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(mobile): the phone agent guide says Wi-Fi names need Location on Always allow

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mobile): final review fixes for the phone runtime's lifecycle and Never

- The bridge checks a category's policy again after Android's prompt, and a recording
  checks it under the lock Never uses to stop one, so Never can't lose a race.
- The host shell records proot's pid before exec'ing it, so a stop can always SIGKILL
  proot; a stop that times out now fails instead of reporting success.
- An exited server's watcher releases the foreground hold only if no newer server
  started, so a quick restart keeps its notification and wake lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant