|
| 1 | +# claude-code-termux — Agent Guide |
| 2 | + |
| 3 | +An aarch64 `.deb` that runs [Claude Code](https://github.com/anthropics/claude-code) |
| 4 | +natively on Termux/Android. Anthropic ships Claude Code only as bun-compiled |
| 5 | +**glibc** ELF binaries with no android-arm64 build, so `npm i -g |
| 6 | +@anthropic-ai/claude-code` is broken on Termux |
| 7 | +([claude-code#50270](https://github.com/anthropics/claude-code/issues/50270)). |
| 8 | +This package installs a launcher that downloads the official `linux-arm64` |
| 9 | +build from Anthropic, patches its ELF interpreter to Termux's glibc loader, |
| 10 | +and routes Claude's embedded-tool re-execs back through the launcher. |
| 11 | + |
| 12 | +## Legal / redistribution |
| 13 | + |
| 14 | +This project ships **only its own code**. The Claude Code binary is downloaded |
| 15 | +directly from Anthropic at install time and patched locally on-device — it is |
| 16 | +**never rehosted** (Anthropic's `LICENSE.md` is "all rights reserved"; their |
| 17 | +Commercial Terms bar duplicating/reselling the Services). Never commit or |
| 18 | +publish the binary; the `.deb` contains no Anthropic bytes. |
| 19 | + |
| 20 | +## How it works |
| 21 | + |
| 22 | +1. `install.sh` enables the `glibc-repo` apt source and `apt install`s the |
| 23 | + `.deb`; `apt` pulls the runtime deps. |
| 24 | +1. `postinst` downloads the `linux-arm64` binary from Anthropic, SHA-256-verifies |
| 25 | + it, runs `patchelf --set-interpreter` to point it at Termux's glibc loader, |
| 26 | + and runs `patch-execpath.py` to blank the subprocess `CLAUDE_CODE_EXECPATH` |
| 27 | + assignment. |
| 28 | +1. The compiled C launcher (`bin/claude`) execs the patched binary — |
| 29 | + `execv` **preserves `argv[0]`** (so Claude's embedded `grep`/`find`/`rg` |
| 30 | + dispatch works), it **clears `LD_PRELOAD`** (so the glibc binary's `ld.so` |
| 31 | + doesn't choke on termux-exec's text-script `libc.so`), and it **sets |
| 32 | + `TMPDIR`/`CLAUDE_CODE_TMPDIR`** to the Termux prefix when unset (Termux has no |
| 33 | + writable `/tmp`; see `src/claude-wrapper.c` for the env-vs-hardcoded-`/tmp` |
| 34 | + analysis and the MCP-browser-bridge limitation, `claude-code#15637`). |
| 35 | +1. `postinst` also merges two keys into `~/.claude/settings.json`: |
| 36 | + `env.LD_PRELOAD` (re-arms termux-exec for subprocess `/usr/bin/env` shebangs) |
| 37 | + and `autoUpdates: false` (so the in-session updater can't clobber the patched |
| 38 | + binary). |
| 39 | + |
| 40 | +## Layout |
| 41 | + |
| 42 | +| Path | Role | |
| 43 | +|---|---| |
| 44 | +| `install.sh` | Single install path. Downloads the latest release `.deb` (or installs a local one via `CLAUDE_CODE_DEB=<path>`), enables `glibc-repo`, `apt install`s it. | |
| 45 | +| `package/control` | Metadata. `Depends: bash, curl, jq, python3, glibc-runner, patchelf-glibc`. | |
| 46 | +| `package/postinst` | Settings merge (skip via `CLAUDE_CODE_SKIP_SETTINGS`) + fetch the binary. | |
| 47 | +| `package/postrm` | Removes the fetched binary on uninstall. | |
| 48 | +| `package/payload/bin/claude-code-termux-update` | Re-patch only if the version changed (`bootstrap.sh update`; schedulable). `--force` to re-apply. | |
| 49 | +| `package/payload/libexec/bootstrap.sh` | Resolve / download (`curl --retry`, optional `CLAUDE_CODE_CACHE_DIR`) / verify / `patchelf` / execPath-patch engine. | |
| 50 | +| `package/payload/libexec/patch-execpath.py` | The `CLAUDE_CODE_EXECPATH` patch. | |
| 51 | +| `package/payload/etc/claude-code-termux.conf` | `CLAUDE_CODE_VERSION` pin + `CLAUDE_CODE_CACHE_DIR` (both empty by default). | |
| 52 | +| `src/claude-wrapper.c` | The C launcher (`-DBINARY=` baked in at compile). | |
| 53 | +| `scripts/build-wrapper.sh` | Compile the wrapper with Termux's clang. | |
| 54 | +| `scripts/build-deb.sh` | Stage + `dpkg-deb --build` → `artifacts/packages/`. | |
| 55 | +| `scripts/test.sh` | Build + install + assert the fixes, inside termux-docker. | |
| 56 | +| `scripts/test-docker.sh` | Run `test.sh` on your machine via Docker (QEMU off-arm64). | |
| 57 | +| `scripts/version.sh` | Prints the version (see Versioning). | |
| 58 | + |
| 59 | +## Versioning |
| 60 | + |
| 61 | +CalVer `YYYY.M.<counter>` via `scripts/version.sh`, where `<counter>` is |
| 62 | +`GITHUB_RUN_NUMBER` (unique + monotonic) or `0` locally. No manual version file. |
| 63 | +Unpadded month (leading zeros read oddly / break some parsers). |
| 64 | + |
| 65 | +## Building & packaging |
| 66 | + |
| 67 | +Hand-assembled and **must run inside `termux/termux-docker:aarch64`** (needs |
| 68 | +Termux's clang + bionic for the wrapper; runs natively on arm64 hosts, under |
| 69 | +QEMU on x86 dev hosts): `build-wrapper.sh` compiles the |
| 70 | +launcher → `build-deb.sh` stages the payload under the Termux prefix and runs |
| 71 | +`dpkg-deb --build`. Output goes to `artifacts/{build,packages}` (git-ignored). |
| 72 | +The version is passed in by the caller; `build-deb.sh`/`test.sh` default to |
| 73 | +`scripts/version.sh`. |
| 74 | + |
| 75 | +## Testing locally |
| 76 | + |
| 77 | +`scripts/test-docker.sh [version]` pulls `termux/termux-docker:aarch64` and runs |
| 78 | +`scripts/test.sh` in it — natively on arm64 hosts, under QEMU elsewhere (needs |
| 79 | +Docker + `binfmt`). `test.sh` builds the |
| 80 | +`.deb`, installs it via `install.sh`, and asserts the real fixes: `argv[0]=rg`/ |
| 81 | +`bfs` dispatch to the embedded ripgrep/bfs, `LD_PRELOAD`-cleared startup, |
| 82 | +`TMPDIR` injection, the `settings.json` merge, and the conditional/cached update |
| 83 | +paths. |
| 84 | + |
| 85 | +For local speed, `test-docker.sh` mounts gitignored host caches under |
| 86 | +`artifacts/cache/` — the Termux **apt archive** cache (so build/runtime `.deb`s |
| 87 | +aren't re-downloaded; apt still fully installs + resolves) and a **`claude`** |
| 88 | +binary cache via `CLAUDE_CODE_CACHE_DIR` (raw pre-patch bytes; `patchelf` + the |
| 89 | +execPath patch still run). CI skips the caches (native arm64 + fast link) but |
| 90 | +sets an ephemeral `CLAUDE_CODE_CACHE_DIR` so the cache-hit path is still tested. |
| 91 | + |
| 92 | +**termux-docker gotcha:** its entrypoint drops privileges and **sanitizes the |
| 93 | +environment**, so `docker run -e VAR=…` does **not** reach the command — set env |
| 94 | +vars **inline** in the `bash -c` string instead. |
| 95 | + |
| 96 | +## CI/CD |
| 97 | + |
| 98 | +- **`ci.yml`** (`pull_request` + `workflow_call`): lints the shell scripts and |
| 99 | + runs the termux build/test job, which builds + installs + tests the `.deb` |
| 100 | + once and uploads it as an artifact. Runs on a **native arm64** runner |
| 101 | + (`ubuntu-24.04-arm`) — no QEMU, so the aarch64 container runs natively. The |
| 102 | + `workflow_call` `claude_version` input (empty = latest) pins which Claude |
| 103 | + Code version the test installs. |
| 104 | +- **`release-watch.yml`** (`schedule: daily` + `workflow_dispatch`): polls |
| 105 | + Anthropic's release channel (the endpoint `bootstrap.sh` resolves against), |
| 106 | + and on a **new** version — gated by an `actions/cache` key per version so it |
| 107 | + runs once per release — calls `ci.yml` pinned to that version. The cache is |
| 108 | + saved only on success (a break is retried daily); a failure opens a |
| 109 | + deduplicated issue. This catches `patch-execpath.py` anchor breaks (bun |
| 110 | + minifier identifier rotation) within a day of release. |
| 111 | +- **`cd.yml`** (`push: main`): reuses `ci.yml`, then a `release` job **gated by |
| 112 | + the `release` GitHub Environment** (configure required reviewers in repo |
| 113 | + settings for the gate to pause) downloads the built artifact and publishes a |
| 114 | + GitHub release. It does **not** rebuild — it ships the exact bytes that were |
| 115 | + tested; the version is read from the `.deb` filename. |
| 116 | + |
| 117 | +## Conventions |
| 118 | + |
| 119 | +- Scripts that run inside termux-docker use the absolute Termux bash shebang |
| 120 | + (`#!/data/data/com.termux/files/usr/bin/bash`); dev-host scripts use |
| 121 | + `#!/usr/bin/env bash`. |
| 122 | +- Releases are CalVer and cut by approving the `release` environment gate on a |
| 123 | + push to `main`; there is no version file to bump. |
0 commit comments