Run OpenCode v2 on Termux (Android) as a native musl binary — installed as plain opencode, exactly like on every other platform.
OpenCode v2 is the official stable line: Node.js runtime, ships via npm (@opencode/cli), with matching v2.x GitHub tags. Upstream docs: opencode.ai/v2.
This repo replaces opencode-termux-musl (v1 + side-by-side era), which is now archived.
Three Termux/Android problems break the naive install; this repo solves all three:
- The official installer misdetects Termux. No
/etc/alpine-release, noldd→ it thinks glibc and downloads the wrong binary. This repo always takes@opencode/cli-linux-arm64-muslfrom npm and patches the ELF interpreter to Termux's musl loader. - DNS is broken for musl/Node on Android. Musl's raw UDP path is blocked; Node's c-ares resolver bypasses even the
libresolvefix.soshim. The wrapper therefore routes outbound HTTPS through a small local Python proxy (scripts/proxy.py) on127.0.0.1:8080. - Spawning
#!/usr/bin/env nodetools fails. The musl wrapper swapsLD_PRELOADaway fromtermux-exec, so shebangs like prettier's never resolve.scripts/enable-formatter.shfixes the formatter (see below).
- Termux (Android 7.0+ / API 24+, aarch64)
curl,tar(installer auto-installspatchelfandclangif missing)python3(for the HTTP proxy)- Internet access to
opencode.ai,registry.npmjs.org, anddl-cdn.alpinelinux.org
Please install from the Copy-Ready link, it's much easier especially if using mobile reddit app (don't copy-paste from the codeblocks below, they're for display):
- install the app Termux:API F-Droid Download, then in Termux:
pkg update && pkg upgrade -y
pkg install termux-api -y
termux-battery-statusThe termux-battery-status line triggers the permission prompt / confirms the bridge works. Skip it and the installer below will refuse with these same steps.
pkg install curl tar python gitcurl -sL https://gist.githubusercontent.com/DEAD1nsane/9b37cba090602f473200fad8b1002e6c/raw/all-in-one.sh | sh
opencode --version
opencode run "say hi"☝🏼Use the gist above — it pulls/updates the repo, runs install.sh, copies all 8 themes (dark + light, 16 files) to ~/.config/opencode/themes/, and schedules auto-update (job 7802, every 6h, applies via --yes path, log ~/.opencode-auto-update.log). Day/night theme-sync is NOT included — opt-in below.
- Resolves the latest v2 version from
https://opencode.ai/update/api/latest/cli/npm - Downloads
@opencode/cli-linux-arm64-muslfrom npm (always musl) - Reuses or installs Alpine musl + libstdc++/libgcc +
libresolvefix.so patchelfs the interpreter to$PREFIX/lib/ld-musl-aarch64.so.1- Installs
$PREFIX/libexec/opencode/opencode-v2-musl.bin+ wrapper$PREFIX/bin/opencode - Installs
$PREFIX/bin/opencode-shared(shared-service mode helper)
Useful knobs:
OPENCODE_VERSION=2.0.30 sh install.sh # pin a version
OPENCODE_WRAPPER_ONLY=1 sh install.sh # refresh wrapper only, no download
OPENCODE_TARBALL_PATH=./cli-linux-arm64-musl-2.0.30.tgz sh install.sh # offline
OPENCODE_STANDALONE=0 sh install.sh # default to shared-service modeFull details, including migration from v1, are in docs/INSTALL.md.
| Check | Command | Status (2.0.30 on Pixel 10) |
|---|---|---|
| Version | opencode --version |
opencode v2.0.30 |
| DNS + API | opencode run "Reply with exactly: PONG" |
works via Python proxy |
| Models / auth | opencode models, opencode auth list |
OpenCode Go key stored |
| Themes | 8 native v2 AMOLED themes in themes/ |
copy + /themes |
| Plugins | v2 format export default { id, setup } |
auto-loaded from ~/.config/opencode/plugins/ |
| MCP | opencode.json mcp block |
Railway / GitHub / Upstash / Chrome DevTools verified |
If run times out with getaddrinfo ETIMEOUT opencode.ai, the network path is broken — check that the wrapper's proxy env is present and proxy.py is running (pgrep -f proxy.py).
Node/c-ares on musl does raw UDP DNS, which Android blocks; libresolvefix.so (which redirects musl's resolv.conf reads to Termux's) does not help Node. The wrapper therefore:
- sets
HTTP(S)_PROXY=http://127.0.0.1:8080(+NO_PROXY=localhost,127.0.0.1,::1) - starts
scripts/proxy.pyin the background if it is not already running - sets
NODE_OPTIONS=--dns-result-order=ipv4first
The proxy adds ~100–200 ms per request — acceptable for interactive use. Local endpoints (MCP servers on localhost, Ollama) bypass it via NO_PROXY.
scripts/proxy.py is a thread-per-connection proxy, so it is deliberately bounded — on Android an unbounded proxy runs out of address space and is killed by the kernel, which surfaces as constant Unable to connect / ConnectionRefused errors:
| Setting | Default | Why |
|---|---|---|
PROXY_MAX_CONNECTIONS |
256 | Above this the proxy answers 503 instead of spawning a thread that cannot start |
PROXY_IDLE_TIMEOUT |
120 s | Bounds how long a handler thread waits for the next request on an idle keep-alive socket |
PROXY_TUNNEL_IDLE_TIMEOUT |
300 s | Idle timeout for a CONNECT tunnel (SSE streams can be silent for minutes) |
PROXY_THREAD_STACK |
524288 | Thread stack bytes — Python's 8 MB default is gigabytes of VmSize at a few hundred connections |
PROXY_REQUEST_QUEUE |
128 | Listen backlog; the default of 5 overflows under load and makes the watchdog kill a healthy proxy |
Two implementation details that matter on this platform:
- the proxy's own outbound requests must not use
HTTP_PROXY/HTTPS_PROXY— it inherits them from the wrapper, so its opener is built with an emptyProxyHandlerto avoid routing through itself; CONNECTtunnels useselect.poll(), notselect.select()—select()raisesfiledescriptor out of range in select()pastFD_SETSIZE(1024) descriptors, which kills every tunnel at once.
If the proxy dies, the watchdog restarts it within 60 s; logcat shows the native abort as shadow stack read-write mprotect(...) failed: Out of memory with Cmdline: .../proxy.py.
--standalone is a per-command flag in v2 (run/serve/web/acp), not a global mode:
- Default (
OPENCODE_STANDALONE=1):opencode run …/opencode serve …get--standaloneinjected → private server per invocation. The TUI (opencodewith no args) attaches to a shared service if one is running. - Shared service: start once with
opencode-shared serve --service(backgrounded); TUI andOPENCODE_STANDALONE=0 opencode run …then attach to it — faster startups, one proxy path. The service picks up proxy env through the wrapper, so start it viaopencode-shared, not by exec'ing the binary directly.
Both modes are verified on-device; pick whichever feels faster day to day.
Eight true-black (#000000) themes in themes/, all in the native OpenCode v2 theme format (explicit categorical hues, so agent pills — Build, Plan, … — keep their intended colors):
mkdir -p ~/.config/opencode/themes
cp themes/*.json ~/.config/opencode/themes/Then pick one with /themes inside OpenCode. The seven accent themes are deliberately restrained — each leans on a single accent color for syntax highlighting on pure black; 8-rainbow-amoled is the exception (white chrome, red links, full neon syntax).
| # | Theme | Accent | Dark | Light |
|---|---|---|---|---|
| 1 | 1-rbw-amoled |
Red #ff2740 |
![]() |
![]() |
| 2 | 2-obw-amoled |
Orange #FF914A |
![]() |
![]() |
| 3 | 3-ybw-amoled |
Yellow #FFFF4B |
![]() |
![]() |
| 4 | 4-gbw-amoled |
Green #00FF7A |
![]() |
![]() |
| 5 | 5-bbw-amoled |
Blue #0090FF |
![]() |
![]() |
| 6 | 6-vbw-amoled |
Violet #6F66FF |
![]() |
![]() |
| 7 | 7-pbw-amoled |
Pink #FF60F8 |
![]() |
![]() |
| 8 | 8-rainbow-amoled |
Full neon (multi) | ![]() |
![]() |
theme is the user-facing command: install.sh puts it at $PREFIX/bin/theme, so after install it works from anywhere — run it with no arguments and it prints this same usage list. From a clone without installing, symlink it instead so edits take effect immediately:
ln -s "$PWD/scripts/theme" ~/bin/themetheme # print this usage list
theme light # force light: opencode theme + white Termux background
theme dark # force dark
theme termux light # only the Termux terminal background, opencode theme untouched
theme termux dark # same, back to black
theme auto # hand control back to the 07:00-19:00 scheduler
theme status # one-glance summary (theme status -v for the full report)theme light|dark cancels the 15-minute scheduler job (7803) first — otherwise the next tick puts you back in dark. Do not force a mode with sh scripts/theme-sync.sh --light|--dark directly: it skips that cancel, so the scheduler overwrites you within 15 minutes. Cancelling the job is the only thing the wrapper adds — all the applying logic (cli.json mode, Termux palette, background/foreground, night-mode) stays in theme-sync.sh. theme termux light is the one to use before a screenshot: it flips the terminal background only, so the TUI cannot re-resolve the theme against a black background and write dark back into cli.json.
The full ANSI palette swap needs ~/.termux/colors-light.properties and ~/.termux/colors-dark.properties; without them it falls back to background/foreground only.
theme.mode: system follows the terminal background (OSC 11), not Android day/night, so on Termux it never auto-switches. scripts/theme-sync.sh does time-based switching instead: light 07:00-19:00, dark otherwise. Theme name is preserved, only mode is rewritten.
What it flips together at 07:00 / 19:00:
- OpenCode
cli.jsontheme.modelight/dark - Termux
~/.termux/colors.propertiesbackground white#ffffffby day / black#000000by night (plus matching foreground), so no black flash behind the TUI - Termux
~/.termux/termux.propertiesnight-modefalse by day / true by night (extra-keys bar + drawer follow this) - the full 16-colour ANSI palette, copied from
~/.termux/colors-light.properties(day) and~/.termux/colors-dark.properties(night) — the dark palette's yellow is 1.07:1 and its white slots 1.00:1 on a white background, i.e. invisible
theme-sync.sh is the engine theme wraps; the commands below assume you are inside the repo directory (after install.sh the same file is at $PREFIX/libexec/opencode/theme-sync.sh, and theme auto registers the job for you):
sh scripts/theme-sync.sh # apply now, based on current time
sh scripts/theme-sync.sh --status # show current vs computed mode
sh scripts/theme-sync.sh --check # print what would change, change nothing
sh scripts/theme-sync.sh --light-start=06:30 --light-end=18:30 # custom window
sh scripts/theme-sync.sh --system # restore mode "system" (Termux left alone)
sh scripts/theme-sync.sh --no-termux # opencode mode only
sh scripts/theme-sync.sh --no-opencode # Termux colors only, leave cli.json alone
sh scripts/theme-sync.sh --no-palette # background/foreground only, keep the ANSI colourscolors-light.properties and colors-dark.properties are the source of truth — edit those, not colors.properties, which the next run overwrites. Override the palette paths with THEME_SYNC_PALETTE_LIGHT / THEME_SYNC_PALETTE_DARK, or set THEME_SYNC_PALETTE=0 to skip the swap entirely.
Unattended (both optional, pick either): job 7803 every 15min, and/or a precmd hook in ~/.zshrc for flips within seconds while the shell is active (see docs/INSTALL.md Theme day/night section). Takes effect on next TUI launch; Termux colors apply immediately but night-mode needs force-stop + reopen. Requires only python3.
The built-in prettier formatter cannot spawn on Termux: its .bin/prettier uses a #!/usr/bin/env node shebang, /usr/bin/env does not exist on Android, and the musl wrapper's LD_PRELOAD swap prevents termux-exec from rewriting it. Format attempts fail with NotFound: ChildProcess.spawn and are silently skipped.
Fix (one time):
sh scripts/enable-formatter.shThis installs prettier globally via Termux npm and points opencode at node <prettier.cjs> --write $FILE — no shebang involved. Only the formatter key of ~/.config/opencode/opencode.json is changed (a .bak backup is written).
Updates are unattended: enable the periodic job once (Android fires it roughly every 6h — not at a fixed hour) and forget it.
termux-job-scheduler --job-id 7802 --period-ms 21600000 \
--network any --persisted true \
-s "$HOME/GitHub/opencode2-termux-musl/scripts/auto-update.sh"The job (needs the Termux:API app — any network, survives reboot) checks the npm channel and re-runs install.sh only when behind; it never downgrades. You get a notification on success or failure (failures name the stuck version and retry automatically on the next run — nothing to do manually). Runs are logged to ~/.opencode-auto-update.log, and the last outcome is always in ~/.opencode-update-status (./scripts/check-update.sh --status shows status + job state + recent log).
Rollback boundary: every update stages the new binary to opencode-v2-musl.bin.new, smoke-tests it there (--version must match the pinned release, run --help must exit 0), and only then promotes it — the previous binary is kept as opencode-v2-musl.bin.last-good with VERSION/SHA256 sidecars. A bad upstream build therefore never overwrites your only working copy. The tarball SHA256 is logged and recorded each install, and enforced when OPENCODE_EXPECTED_SHA256 is set. If you ever need to go back by hand:
./scripts/check-update.sh --rollback # restore last-known-good binaryManual options, if you ever need them:
./scripts/check-update.sh # check only (exit 2 + notification if behind)
./scripts/check-update.sh --status # last-run status + periodic-job state + recent log
./scripts/check-update.sh --yes # check + re-run install.sh if behind
./scripts/check-update.sh --rollback # restore last-known-good binaryv2 updates come from the npm channel only; the script compares against opencode.ai/update/api/latest/cli/npm.
Migrating the v1-era job (7801/7802 pointed at the archived opencode-termux-musl repo): re-enable the same job id with the path above — the v1 script compares against v1 GitHub tags and silently exits "nothing to do" once opencode reports 2.x.
If you previously ran v1 (opencode 1.x) or the side-by-side setup (opencode2):
cd opencode2-termux-musl
sh install.sh # v2 takes over the `opencode` name
sh scripts/remove-v1.sh # deletes v1 binary + opencode2 leftovers (~350 MB)remove-v1.sh refuses to run unless opencode --version already reports v2 and the v2 binary is in place. It never touches session data (~/.local/share/opencode) or config (~/.config/opencode). Your v1 sessions live in the same data directory and appear under v2.
- Formatter: needs
enable-formatter.shonce (shebang issue above). Without it, formatting silently no-ops. - Theme
systemmode: follows the terminal background (OSC 11), not the Android day/night mode — Termux's background is fixed, sosystemnever auto-switches. Usescripts/theme-sync.shfor time-based light/dark (see Light/dark auto-switch above). - PTY support: depends on
librust_pty_arm64.so; not shipped yet. PRs welcome. - Proxy latency: ~100–200 ms per outbound request (see Network above).
- Pixel 10 (Android 17, Termux 0.119, aarch64) — OpenCode v2.0.30 as
opencode: TUI,run, shared service, themes, MCP servers (Railway, GitHub, Upstash, Chrome DevTools), and the v2 plugin API all verified on-device.
- guysoft/opencode-termux — original Termux packaging ideas
- DEAD1nsane/opencode-termux-musl — v1 musl era + side-by-side v2 bring-up (archived)
- anomalyco/opencode — OpenCode itself















