Skip to content

Instantly share code, notes, and snippets.

@GGPrompts
Last active July 6, 2026 06:30
Show Gist options
  • Select an option

  • Save GGPrompts/73bcc5b9d22c71d6ea926ca609f43823 to your computer and use it in GitHub Desktop.

Select an option

Save GGPrompts/73bcc5b9d22c71d6ea926ca609f43823 to your computer and use it in GitHub Desktop.
Termux Fixes: Running AI CLI Tools (Claude Code, Codex) on Android — updated July 2026 for the claude-code native-binary era (2.1.114+)

Termux Fixes: Running AI CLI Tools on Android

Hard-won fixes for running Claude Code, OpenAI Codex, GitHub Copilot, and Rust PTY apps on Termux/Android.

⚠️ July 2026 update: Section 2 (Claude Code via Bun) is obsolete — Anthropic removed cli.js from the npm package in 2.1.114 (April 17, 2026), so there is no JS entrypoint to run under Bun/Node anymore. See Section 5 for the native-binary era fixes (glibc loader wrapper, DNS via nsswitch.conf, hook shebang breakage, login workaround, and the safe update procedure). Sections 1, 3, and the general tips still apply.


1. Making portable-pty Work on Android/Termux

The Problem

Rust's portable-pty crate depends on the termios crate for PTY operations. The termios crate (v0.2.2) only recognizes target_os = "linux", but Rust on Android reports target_os = "android". Since Android uses identical termios structs and POSIX PTY syscalls to Linux, this is purely a target detection issue — not a compatibility one.

The Fix: 2 Changes

1. Patch the termios crate (the critical fix)

Create a local fork of termios and modify src/os/mod.rs:

// BEFORE (original termios 0.2.2)
#[cfg(target_os = "linux")] pub use self::linux as target;
#[cfg(target_os = "linux")] pub mod linux;

// AFTER (with Android support)
#[cfg(any(target_os = "linux", target_os = "android"))] pub use self::linux as target;
#[cfg(any(target_os = "linux", target_os = "android"))] pub mod linux;

Then in your project's Cargo.toml, patch the dependency:

[patch.crates-io]
termios = { path = "termios-patch" }

That's it. portable-pty's native_pty_system() now works on Android.

2. Fix hardcoded /tmp paths

Termux doesn't have access to /tmp. Replace all hardcoded /tmp references with $TMPDIR:

Rust:

fn tmp_dir() -> std::path::PathBuf {
    std::env::var("TMPDIR")
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|_| std::path::PathBuf::from("/tmp"))
}

// Use: tmp_dir().join("myapp.log") instead of "/tmp/myapp.log"

Shell scripts:

STATE_DIR="${TMPDIR:-/tmp}/my-state"
LOG_FILE="${TMPDIR:-/tmp}/myapp.log"

Why This Works

  • Android's kernel exposes standard POSIX PTY syscalls (openpty, forkpty, etc.)
  • The termios struct layout is identical between Linux and Android
  • All FFI calls in portable-pty resolve to libc functions available on both platforms
  • The only barrier was Rust's #[cfg(target_os)] not matching "android"

Architecture

Android App → WebSocket → Rust/Axum Backend → [Patched termios] → portable-pty → PTY → Shell/Claude Code

2. Running Claude Code with Bun on Termux — ⚠️ OBSOLETE (pre-2.1.114)

This entire section stopped working with @anthropic-ai/claude-code@2.1.114 (2026-04-17), which removed cli.js from the npm package. Kept for historical reference — the env-preload and ld.so argument-quoting tricks are still useful patterns for other Bun/Node tools. For current Claude Code, see Section 5.

The Problem

Claude Code installs via npm and runs on Node.js, but startup is slow (~0.73s). Bun is much faster (~0.41s), but:

  1. Bun on Termux requires glibc-runner (Termux uses musl/bionic, Bun needs glibc)
  2. grun (glibc-runner's wrapper) has unquoted $@ that word-splits arguments with spaces, breaking -p "multi word prompt"
  3. Bun drops environment variables when run through glibc's ld.so
  4. Claude Code needs CLAUDE_CODE_TMPDIR since /tmp doesn't exist on Termux

The Fix: claude-fast wrapper + env-preload.js

Wrapper script (~/.bun/bin/claude-fast)

Bypass grun entirely by calling ld.so directly — this preserves argument quoting:

#!/bin/bash
export CLAUDE_CODE_TMPDIR="${TMPDIR:-/data/data/com.termux/files/usr/tmp}"
exec /data/data/com.termux/files/usr/glibc/lib/ld-linux-aarch64.so.1 \
  --library-path /data/data/com.termux/files/usr/glibc/lib \
  /data/data/com.termux/files/home/.bun/bin/buno \
  --preload /data/data/com.termux/files/home/.bun/bin/env-preload.js \
  /data/data/com.termux/files/usr/lib/node_modules/@anthropic-ai/claude-code/cli.js \
  "$@"

Environment restoration (env-preload.js)

When Bun runs through ld.so, it loses all environment variables. This preload script restores them from /proc/self/environ:

// env-preload.js — Bun preload to restore env vars lost via ld.so
import { readFileSync } from "fs";

// Restore env vars from /proc/self/environ (null-byte separated)
try {
  const raw = readFileSync("/proc/self/environ", "utf8");
  for (const entry of raw.split("\0")) {
    const eq = entry.indexOf("=");
    if (eq > 0) {
      const key = entry.substring(0, eq);
      const val = entry.substring(eq + 1);
      if (!(key in process.env)) process.env[key] = val;
    }
  }
} catch {}

// Fix argv[0] and execPath for Claude Code's self-detection
process.argv[0] = process.env._ || "claude";
process.execPath = "/data/data/com.termux/files/home/.bun/bin/buno";

// Fix stderr.isTTY (Bun sometimes loses this through ld.so)
if (process.stderr && !process.stderr.isTTY && process.stdout.isTTY) {
  Object.defineProperty(process.stderr, "isTTY", { value: true });
}

// Set TMPDIR for Claude Code (Termux has no /tmp)
if (!process.env.CLAUDE_CODE_TMPDIR) {
  process.env.CLAUDE_CODE_TMPDIR = process.env.TMPDIR || "/data/data/com.termux/files/usr/tmp";
}

Key Details

  • Why not grun: It has exec $GR_BINARY $@ (unquoted!) which word-splits -p "multi word prompt" into -p, "multi, word, prompt"
  • Performance: Bun via ld.so = 0.41s startup vs Node.js = 0.73s (44% faster per spawn)
  • Don't run claude install on Termux — it downloads Anthropic's custom Bun binary which segfaults on Android
  • Disable auto-update in ~/.claude/config.json: {"autoUpdater": {"disabled": true}}

3. Running OpenAI Codex CLI on Termux

The Problem

Codex CLI installs via npm and includes a statically-linked musl Rust binary (codex_core) at:

@openai/codex-linux-arm64/vendor/aarch64-unknown-linux-musl/codex/codex

The binary starts, shows the session header, but every API call fails with:

error sending request for url (https://chatgpt.com/backend-api/codex/responses)

Meanwhile, curl https://chatgpt.com works fine from the same Termux session.

Root Cause: musl DNS Resolution

Statically-linked musl hardcodes DNS resolution via /etc/resolv.conf. On Android/Termux:

  • /etc/ is a read-only system partition
  • There is no /etc/resolv.conf
  • musl falls back to querying 127.0.0.1:53 — no DNS server there
  • Every hostname lookup silently fails → connection timeouts

curl works because Termux's curl uses Android's native DNS resolver, not musl's.

The Fix: proot bind-mount

Use proot to fake /etc/resolv.conf for the musl binary:

# Create a resolv.conf in your home directory
printf 'nameserver 8.8.8.8\nnameserver 1.1.1.1\n' > ~/resolv.conf

Wrapper script (~/.bun/bin/codex-fast)

#!/bin/bash
# codex-fast: wrapper for Codex CLI on Termux
# Fixes DNS resolution for statically-linked musl binary via proot
# and sets SSL cert path for Termux

RESOLV_CONF="$HOME/resolv.conf"

# Ensure resolv.conf exists
if [ ! -f "$RESOLV_CONF" ]; then
  printf 'nameserver 8.8.8.8\nnameserver 1.1.1.1\n' > "$RESOLV_CONF"
fi

export SSL_CERT_FILE=/data/data/com.termux/files/usr/etc/tls/cert.pem
exec proot -b "$RESOLV_CONF:/etc/resolv.conf" codex "$@"

Auth Setup

Codex uses ChatGPT OAuth (not OpenAI API keys). Auth on Termux's browser doesn't work, so copy credentials from desktop:

# On desktop: login first
codex login

# Copy auth to phone (via SSH/Tailscale)
cat ~/.codex/auth.json | base64 | ssh phone-ip -p 8022 'base64 -d > ~/.codex/auth.json && chmod 600 ~/.codex/auth.json'

# Phone config (~/.codex/config.toml)
sandbox_mode = "danger-full-access"
cli_auth_credentials_store = "file"

[projects."/data/data/com.termux/files/home"]
trust_level = "trusted"

Usage

# Install proot if not present
pkg install proot

# Non-interactive mode (like claude -p)
codex-fast exec "What is 2+2?"

# Without trust config, add --skip-git-repo-check
codex-fast exec --skip-git-repo-check "prompt here"

Gotchas

  • Codex eats Ctrl+C during connection retries — use killall codex from another session
  • Auth tokens expire ~10 days — re-run codex login on desktop and re-copy
  • The failed to refresh available models: timeout errors are non-fatal (model list fetching)
  • Codex uses chatgpt.com/backend-api/codex/responses (ChatGPT Plus subscription), NOT api.openai.com

4. Running GitHub Copilot CLI on Termux

The Problem

GitHub Copilot CLI (@github/copilot v1.0.3) installs via npm and works on Node.js, but crashes immediately:

Error: Cannot find module './pty.node'

Node.js native PTY modules have never worked on Termux/Android. There are no android-arm64 builds of pty.node or node-pty, and the linux-arm64 builds are glibc-linked so they can't be dlopen()'d from Termux's bionic Node.js. This is the same fundamental problem that led to Section 1's approach — using Rust's portable-pty (with the patched termios crate) instead of Node.js PTY bindings.

Root Cause

The loadNativeModule() function in Copilot's app.js throws when it can't find a compatible pty.node. This crashes the entire CLI — even in non-interactive (-p) mode where PTY is never actually used.

The Fix: Patch loadNativeModule() to return null

Since -p mode (non-interactive prompt) doesn't need PTY, we can stub out the native module loader:

# Find Copilot's app.js
APP_JS=$(node -e "console.log(require.resolve('@github/copilot/dist/app.js'))")

# Back it up first (re-apply after copilot updates)
cp "$APP_JS" "${APP_JS}.bak"

Edit app.js and find the loadNativeModule function. Replace its body so it returns a null stub instead of throwing:

// BEFORE (original)
function loadNativeModule() {
  // ... platform detection logic ...
  // throws if no compatible binary found
}

// AFTER (patched for Termux)
function loadNativeModule() {
  return { dir: null, module: null };
}

The rest of the CLI handles module: null gracefully — it just skips PTY allocation and uses stdio instead, which is exactly what -p mode needs.

Auth Setup

Copilot CLI uses GitHub authentication. The easiest approach on Termux is to reuse gh CLI credentials:

# Install gh CLI if not present
pkg install gh

# Login once
gh auth login

# Use with Copilot — pass the token via env var
GITHUB_TOKEN=$(gh auth token) copilot -sp "your prompt"

Usage

# Install
npm install -g @github/copilot

# Apply the patch (see above)

# Non-interactive mode: -s (silent/response only) -p (prompt)
GITHUB_TOKEN=$(gh auth token) copilot -sp "Explain how DNS works"

# You can alias it
alias copilot-fast='GITHUB_TOKEN=$(gh auth token) copilot -sp'
copilot-fast "What does this error mean?"

Gotchas

  • Re-apply patch after updates: copilot update overwrites app.js — keep the .bak and re-patch
  • No interactive mode: The PTY stub means interactive terminal features won't work — only -p (prompt) mode
  • GitHub auth required: Needs an active GitHub Copilot subscription (Individual, Business, or Enterprise)
  • -s flag: Silent mode — suppresses spinner/progress and returns only the response text

5. Claude Code Native-Binary Era (2.1.114+, April 2026)

What Changed Upstream

As of @anthropic-ai/claude-code@2.1.114 (published 2026-04-17; 2.1.112 was the last version with cli.js — verified by diffing registry tarballs), the npm package no longer ships JavaScript. It contains only a launcher (bin/claude.exe, cli-wrapper.cjs, install.cjs) that hands off to a platform-native binary from an optional dependency (@anthropic-ai/claude-code-linux-arm64 etc.).

Two problems on Termux:

  1. npm on Termux reports os: android, so the linux-arm64 optional dep never auto-installs — claude becomes a 12-line error stub.
  2. Even installed, the binary is a glibc ELF (interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux). Android is bionic — it cannot exec natively.

If you were pinned on an old version with the autoupdater disabled, everything kept working until the day you ran claude update — then it all broke at once. Claude Code is now in the same boat Codex always was: a native binary needing a compatibility shim.

The Fix: glibc-loader wrapper

Install the Termux glibc prefix (pkg install glibc-repo && pkg install glibc-runner, which provides $PREFIX/glibc), then wrap the official binary (~/.local/bin/claude-fast or similar, on PATH):

#!/data/data/com.termux/files/usr/bin/bash
# Run the official claude-code linux-arm64 binary through Termux's glibc loader.

CLAUDE_BIN="/data/data/com.termux/files/usr/lib/node_modules/@anthropic-ai/claude-code-linux-arm64/claude"
LD_SO="/data/data/com.termux/files/usr/glibc/lib/ld-linux-aarch64.so.1"
GLIBC_PREFIX="/data/data/com.termux/files/usr/glibc"

unset LD_PRELOAD
export PATH="/data/data/com.termux/files/usr/bin:${GLIBC_PREFIX}/bin:${PATH}"
export SHELL=/data/data/com.termux/files/usr/bin/bash

exec "$LD_SO" "$CLAUDE_BIN" "$@"

unset LD_PRELOAD is required (termux-exec's bionic shim can't be preloaded into a glibc process) — but it has a side effect covered below.

Gotcha 1: DNS hang — "checking connectivity…" forever

The glibc binary resolves hostnames through glibc's NSS, which needs $PREFIX/glibc/etc/nsswitch.conf. Termux ships a resolv.conf there but no nsswitch.conf, so glibc never does DNS and the connectivity preflight hangs forever. Fix:

cat > $PREFIX/glibc/etc/nsswitch.conf <<'EOF'
passwd: files
group: files
shadow: files
hosts: files dns
networks: files
protocols: files
services: files
EOF

(Same disease as Section 3's musl /etc/resolv.conf issue, different libc. Test with claude-fast -p hi — it should reach an auth prompt or answer, not hang. getent may not be installed; don't use it to test.)

Gotcha 2: hooks break — #!/bin/bash shebangs

With LD_PRELOAD unset, termux-exec's shebang rewriting is gone. Any hook/statusline script starting with #!/bin/bash dies with exit 127 ("required file not found") because /bin/bash doesn't exist on Android. Under the old Node-based cli.js this worked invisibly.

Fix: every script Claude Code spawns must use the full Termux shebang:

#!/data/data/com.termux/files/usr/bin/bash

Gotcha 3: interactive login hangs → "Claude Code might not be available in your country"

The login/onboarding preflight hangs on "checking network connection" and then shows a canned "not available in your country" error. This is not geo-blocking and not a real network failure — authenticated API calls work fine from the same binary. Workaround: skip the login flow by copying credentials from an already-logged-in desktop:

# desktop → phone (Tailscale/SSH)
scp -P 8022 ~/.claude/.credentials.json phone-ip:~/.claude/.credentials.json

Also copy the oauthAccount object into the phone's ~/.claude.json and set hasCompletedOnboarding: true. The token is account-scoped (no device binding) and auto-renews via its refreshToken — renewal is a plain API call that does NOT go through the broken login preflight, so it keeps working indefinitely once seeded.

Updating safely

Never run claude update or claude install on Termux — the self-installer only knows official platforms (and its Bun build segfaults on Android). The npm route needs two steps because of the os: android mismatch:

npm update -g @anthropic-ai/claude-code --force
CLAUDE_VER=$(npm ls -g @anthropic-ai/claude-code --json | node -pe "JSON.parse(require('fs').readFileSync(0)).dependencies['@anthropic-ai/claude-code'].version")
npm install -g "@anthropic-ai/claude-code-linux-arm64@${CLAUDE_VER}" --force

Skip the second step and your wrapper runs a stale binary. Note npm rewrites $PREFIX/bin/claude (back to the error stub) on every update — keep your own claude → claude-fast symlink in a directory earlier on PATH (e.g. ~/.local/bin or ~/.bun/bin), which npm never touches. Set "env": {"DISABLE_AUTOUPDATER": "1"} in ~/.claude/settings.json so updates only happen when you choose.


General Termux Tips for CLI Tools

  • No /tmp: Always use $TMPDIR (usually /data/data/com.termux/files/usr/tmp)
  • No /etc/resolv.conf: Statically-linked binaries (musl, Go) that read this will fail DNS. Fix with proot bind-mount.
  • glibc binaries and DNS: the glibc-prefix equivalent of the above — glibc NSS needs $PREFIX/glibc/etc/nsswitch.conf with a hosts: files dns line, which Termux doesn't ship. Symptom: HTTPS by IP works, hostnames hang forever.
  • glibc binaries and shebangs: running a glibc binary means unset LD_PRELOAD, which kills termux-exec's shebang rewriting for everything it spawns. Child scripts must use full Termux shebangs (#!/data/data/com.termux/files/usr/bin/bash), not #!/bin/bash or #!/usr/bin/env bash.
  • SSL certs: Termux keeps them at $PREFIX/etc/tls/cert.pem. Set SSL_CERT_FILE for tools that don't find them.
  • glibc binaries: Use glibc-runner package, but call ld.so directly if you need proper argument quoting.
  • Read-only /etc: Use proot -b to bind-mount files into read-only paths.
  • Native module failures: If a Node.js CLI crashes loading a .node binary, check if the feature it provides is actually needed for your use case. Often you can stub it out for non-interactive modes.

Project

Discovered while building CodeFactory — a mobile-first terminal/IDE app running in Termux with xterm.js frontend and Rust backend.

@EuneiroByte

EuneiroByte commented May 16, 2026

Copy link
Copy Markdown

🥰 大佬太强啦!拯救了我的 Termux AI 环境!

手机端折腾 Termux 跑 AI 工具真的踩了无数的坑,看到这份 Hard-won fixes 简直像看到了救星一样,特来膜拜并反馈环境!✨

🔧 核心痛点 💡 大佬的神级解法 (核心亮点) 🟢 运行状态
Rust PTY 报错 修补 termios 绕过 target_os 限制 Fix 成功 🌟
musl 网络解析失败 利用 proot 绑定 /etc/resolv.conf 解决 DNS 网络畅通 🌐
Bun 环境丢失/卡顿 绕过 grun 直接用 ld.so 引导,白嫖 44% 性能 起飞 🚀

📱 测试环境反馈

  • 设备机型Redmi K80 至尊版
  • 系统版本Android 16

💖 一点小结: 感谢这么硬核又无私的分享,每一个 Gotchas 都写得清清楚楚。思路太清晰啦,默默收藏,给大佬递茶~ 🍵✨

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment