#!/data/data/com.termux/files/usr/bin/bash # claude-code-android installer (Termux on aarch64 Android). # # Installs Anthropic's official linux-arm64 claude binary, patched via # glibc-runner so it runs under Android's bionic kernel. A wrapper at # $PREFIX/bin/claude auto-checks for new versions once per day on launch # (--update-now forces an immediate check) and re-patches if needed. # # Two yes/no questions up front, then unattended. Approx 5-10 minutes # depending on connection. The first download is ~233 MB. # # Re-running this script is not supported. It is a fresh-install path. # If you need to update claude, that happens automatically through the # wrapper. If you want to start over, run termux-reset then re-run. # # Tracking the upstream issue this works around: # https://github.com/anthropics/claude-code/issues/50270 set -euo pipefail info(){ printf '\033[0;36m[info]\033[0m %s\n' "$1"; } ok(){ printf '\033[0;32m[ok]\033[0m %s\n' "$1"; } warn(){ printf '\033[0;33m[warn]\033[0m %s\n' "$1" >&2; } fail(){ printf '\033[0;31m[fail]\033[0m %s\n' "$1" >&2; exit 1; } # --- Preflight --- [ -z "${PREFIX:-}" ] && fail "PREFIX unset. Run this inside Termux, not adb shell." [ "$(uname -m)" = "aarch64" ] || fail "aarch64 only. uname -m reports: $(uname -m)" # Android's low-memory killer can SIGKILL the whole process tree during the heavy # glibc install if this runs inside a claude session under memory pressure. A # plain Termux shell is safer. if [ -n "${CLAUDE_CODE_EXECPATH:-}" ] || [ -n "${CLAUDECODE:-}" ]; then warn "You appear to be running inside a claude session; Android may kill the" warn "install under memory pressure. A plain Termux shell is safer." read -r -p "Continue anyway? [y/N] " LMK case "${LMK,,}" in y|yes) ;; *) fail "Stopped. Open a fresh Termux session and re-run." ;; esac fi # --- Classify any prior claude state, then route or pick an install mode --- # One classifier covers every real prior state instead of a blunt # "anything-exists, refuse" gate. Outcomes: # already_v29 complete v2.9.0 wrapper present -> nothing to do # pinned npm @anthropic-ai/claude-code present -> migrate.sh (safe npm removal) # inplace official native install, or leftover ~/.claude with no working # binary -> install here, preserving data # fresh no claude footprint at all -> clean install CC_NPM_PKG="$PREFIX/lib/node_modules/@anthropic-ai/claude-code" CC_BINLINK="$PREFIX/bin/claude" CC_VERSIONS="$HOME/.local/share/claude/versions" cc_has_versions(){ [ -d "$CC_VERSIONS" ] && ls "$CC_VERSIONS"/*.*.* >/dev/null 2>&1; } cc_is_wrapper(){ [ -f "$CC_BINLINK" ] && [ ! -L "$CC_BINLINK" ]; } cc_is_npm_link(){ [ -L "$CC_BINLINK" ] && readlink "$CC_BINLINK" | grep -q 'node_modules/@anthropic-ai/claude-code'; } if cc_has_versions && cc_is_wrapper; then state="already_v29" elif [ -d "$CC_NPM_PKG" ] || cc_is_npm_link; then state="pinned" elif cc_has_versions || [ -e "$HOME/.local/bin/claude" ] || [ -d "$HOME/.local/share/claude" ] \ || [ -e "$HOME/.claude" ] || [ -e "$HOME/.claude.json" ]; then state="inplace" else state="fresh" fi if [ "$state" = already_v29 ]; then ok "claude is already installed via the v2.9.0 wrapper. Nothing to do here." info "The wrapper auto-updates daily. Force a check now with: claude --update-now" exit 0 fi if [ "$state" = pinned ]; then info "An older pinned v2.x install is present." info "To upgrade WITHOUT losing your sessions, login, or settings, use the" info "migration script instead of this installer:" printf '\n curl -fsSL https://raw.githubusercontent.com/ferrumclaudepilgrim/claude-code-android/main/migrate.sh -o migrate.sh\n bash migrate.sh\n\n' info "This installer does not remove npm installs; migrate.sh does that safely." exit 0 fi cat <" prompts. Packages: git, gh, wget, jq, python, openssh, tree, proot, termux-api, proot-distro, make, clang, file, xxd, htop, bat, fzf (17 packages, roughly 200 MB additional disk). Q2 read -r -p "Install recommended packages? [Y/n] " Q2 Q2="${Q2:-Y}" case "${Q2,,}" in y|yes) RECOMMENDED=1 ;; n|no) RECOMMENDED=0 ;; *) fail "Q2: answer 'y' or 'n'; got '$Q2'" ;; esac ok "Q2: $([ $RECOMMENDED = 1 ] && echo yes || echo no)" echo # --- Pre-install: fresh asserts, or in-place preservation --- if [ "$state" = inplace ]; then # A prior claude config is present (official native install, or a leftover # ~/.claude after a removed claude). Install in place and keep the user's # data: ~/.claude (sessions, login, agents, hooks) is never removed, and # settings.json is merged, not overwritten. RUNNING="$( { pgrep -x claude; pgrep -f '@anthropic-ai/claude-code'; } 2>/dev/null | sort -un | grep -vw "$$" | grep -vw "${PPID:-0}" | tr '\n' ' ' || true )" if [ -n "${RUNNING// /}" ]; then fail "claude appears to be running (PIDs: $RUNNING). Close all claude sessions, then re-run." fi if [ -e "$HOME/.claude/settings.json" ]; then cp -a "$HOME/.claude/settings.json" "$HOME/.claude/settings.json.pre-v29.bak" 2>/dev/null \ && ok "backed up existing settings.json -> settings.json.pre-v29.bak" fi ok "existing claude config will be preserved (installing in place)" else # Fresh: the classifier already proved there is no claude footprint; these are # belt-and-suspenders guards against a race or a partial earlier run. [ -e "$PREFIX/bin/claude" ] && fail "\$PREFIX/bin/claude already exists. Use migrate.sh, or 'termux-reset' for a clean install." [ -e "$HOME/.local/share/claude" ] && fail "\$HOME/.local/share/claude already exists. Use migrate.sh for an in-place upgrade." ok "clean state confirmed" fi # --- apt non-interactive options based on Q1 --- export DEBIAN_FRONTEND=noninteractive if [ "$FRESH" = 1 ]; then APT_OPTS="-y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confnew" else APT_OPTS="-y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold" fi # --- Pin a Termux mirror if none is selected (avoids an interactive stall) --- # On a brand-new Termux with no chosen mirror, the package tooling can stop on a # mirror-selection prompt. Selecting the default first keeps the run unattended. # Only acts when nothing is chosen yet, so it never overrides a working mirror. if [ ! -e "$PREFIX/etc/termux/chosen_mirrors" ] && [ -e "$PREFIX/etc/termux/mirrors/default" ]; then ln -sf "$PREFIX/etc/termux/mirrors/default" "$PREFIX/etc/termux/chosen_mirrors" 2>/dev/null || true fi # --- Termux: bring base packages current --- # apt-get (not pkg/apt) for the scripted steps: apt-get has a stable CLI and # does not print apt's "does not have a stable CLI interface" script warning. info "apt-get update" apt-get update $APT_OPTS >/dev/null || fail "apt-get update failed" info "apt-get full-upgrade (fixes any bootstrap/current library mismatches)" apt-get full-upgrade $APT_OPTS >/dev/null || fail "apt-get full-upgrade failed" info "apt-get install curl jq" apt-get install $APT_OPTS curl jq >/dev/null || fail "apt-get install curl/jq failed" ok "base tools installed" # --- glibc-runner + patchelf-glibc --- info "apt-get install glibc-repo (enables Termux glibc-packages source)" apt-get install $APT_OPTS glibc-repo >/dev/null || fail "glibc-repo install failed" apt-get update $APT_OPTS >/dev/null || fail "apt-get update after glibc-repo failed" info "apt-get install glibc-runner patchelf-glibc (~50 MB download)" apt-get install $APT_OPTS glibc-runner patchelf-glibc >/dev/null || fail "glibc-runner install failed" PATCHELF="$PREFIX/glibc/bin/patchelf" GLIBC_LD="$PREFIX/glibc/lib/ld-linux-aarch64.so.1" [ -x "$PATCHELF" ] || fail "patchelf not found at $PATCHELF after install" [ -f "$GLIBC_LD" ] || fail "glibc ld.so not found at $GLIBC_LD after install" ok "glibc-runner + patchelf installed" # --- Resolve latest claude version, download, verify, patch --- info "resolving latest claude version from npm registry" LATEST="$(curl -fsSL --max-time 10 https://registry.npmjs.org/@anthropic-ai/claude-code/latest 2>/dev/null | jq -r .version 2>/dev/null)" if [ -z "$LATEST" ] || [ "$LATEST" = "null" ]; then fail "could not query npm registry for the latest claude version" fi if ! printf '%s' "$LATEST" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then fail "npm registry returned an unexpected version string: $LATEST" fi ok "latest claude version: $LATEST" VERSIONS_DIR="$HOME/.local/share/claude/versions" BINARY="$VERSIONS_DIR/$LATEST" WRAPPER="$PREFIX/bin/claude" mkdir -p "$VERSIONS_DIR" "$HOME/.claude" DL_BASE="https://downloads.claude.ai/claude-code-releases/$LATEST" info "downloading $LATEST linux-arm64 binary (~233 MB)" curl -fsSL --max-time 300 "$DL_BASE/linux-arm64/claude" -o "$BINARY.tmp" \ || fail "binary download failed" info "verifying checksum against published manifest" EXP="$(curl -fsSL --max-time 10 "$DL_BASE/manifest.json" 2>/dev/null | jq -er '.platforms["linux-arm64"].checksum' 2>/dev/null || true)" ACT="$(sha256sum "$BINARY.tmp" | cut -d' ' -f1)" if [ -z "$EXP" ]; then rm -f "$BINARY.tmp" fail "could not read checksum from manifest" fi if [ "$EXP" != "$ACT" ]; then rm -f "$BINARY.tmp" fail "checksum mismatch: expected $EXP, got $ACT" fi ok "checksum verified" chmod +x "$BINARY.tmp" LD_PRELOAD='' "$PATCHELF" --set-interpreter "$GLIBC_LD" "$BINARY.tmp" \ || fail "patchelf failed to set ELF interpreter" mv "$BINARY.tmp" "$BINARY" ok "binary patched and installed at $BINARY" # --- ~/.claude/settings.json --- # autoUpdates:false disables claude's in-process updater; the wrapper handles # updates instead. No env.LD_PRELOAD: a bionic preload set here leaks into the # Bash tool's subprocesses and breaks claude's bundled grep/rg/ugrep, which # re-exec the raw glibc binary and then mis-resolve libc. The wrapper already # clears LD_PRELOAD before exec, so the binary itself is unaffected. # Known trade-off: without the preload, claude's subprocesses also lose # termux-exec, so a directly-run "#!/usr/bin/env ..." script cannot find its # interpreter (Android has no /usr/bin/env). Grep correctness wins; the common # cases (bash/python/node FILE, and tools called by name) still work. SF="$HOME/.claude/settings.json" if [ -e "$SF" ]; then TMP="$(mktemp "${TMPDIR:-$PREFIX/tmp}/cc-settings.XXXXXX")" if jq 'del(.env.LD_PRELOAD) | .autoUpdates=false | if (.env // {}) == {} then del(.env) else . end' "$SF" > "$TMP" 2>/dev/null; then cat "$TMP" > "$SF" # write THROUGH a possible symlink rather than replacing it rm -f "$TMP" ok "settings.json updated (existing keys preserved; stale LD_PRELOAD removed)" else rm -f "$TMP" warn "settings.json is not valid JSON; leaving it untouched." warn "Set \"autoUpdates\": false by hand and remove any env.LD_PRELOAD." fi else cat > "$SF" <<'EOF' { "autoUpdates": false } EOF ok "settings.json written" fi # --- Wrapper at $PREFIX/bin/claude --- # Once per 24h on launch, checks npm for a newer version. If found, # downloads, verifies checksum, patchelfs, swaps. --update-now forces # an immediate check, bypassing the rate limit. Any failure (network, # checksum, patchelf) is reported to stderr and the cached binary is # used. Self-heals the ELF interpreter every launch. Unsets LD_PRELOAD # before exec so the glibc binary doesn't crash on libtermux-exec's # unversioned libc.so dependency. cat > "$WRAPPER" </dev/null || echo 0) [ \$((now - last)) -ge \$RATE_LIMIT ] && should_check=1 fi if [ "\$should_check" = 1 ]; then latest=\$(curl -fsSL --max-time 5 https://registry.npmjs.org/@anthropic-ai/claude-code/latest 2>/dev/null | jq -r .version 2>/dev/null || echo "") if [ -n "\$latest" ] && printf '%s' "\$latest" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\$'; then new_bin="\$VERSIONS_DIR/\$latest" if [ ! -f "\$new_bin" ]; then dl="https://downloads.claude.ai/claude-code-releases/\$latest" if curl -fsSL --max-time 300 "\$dl/linux-arm64/claude" -o "\$new_bin.tmp" 2>/dev/null; then exp=\$(curl -fsSL --max-time 5 "\$dl/manifest.json" 2>/dev/null | jq -er '.platforms["linux-arm64"].checksum' 2>/dev/null || echo "") act=\$(sha256sum "\$new_bin.tmp" | cut -d' ' -f1) if [ -n "\$exp" ] && [ "\$exp" = "\$act" ]; then chmod +x "\$new_bin.tmp" if LD_PRELOAD= "\$PATCHELF" --set-interpreter "\$GLIBC_LD" "\$new_bin.tmp" 2>/dev/null; then mv "\$new_bin.tmp" "\$new_bin" # Retain N-1 (latest + previous) for rollback. If the new \$latest # ships broken, "rm versions/\$latest && claude --update-now" puts # you back on the prior known-good binary. prev=\$(ls -1 "\$VERSIONS_DIR" 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\$' | sort -V | tail -2 | head -1) for old in "\$VERSIONS_DIR"/*; do base=\$(basename "\$old") [ -f "\$old" ] && [ "\$base" != "\$latest" ] && [ "\$base" != "\$prev" ] && rm -f "\$old" done else rm -f "\$new_bin.tmp" echo "[claude] update: patchelf failed on \$latest, using cached" >&2 fi else rm -f "\$new_bin.tmp" echo "[claude] update: checksum mismatch on \$latest, using cached" >&2 fi else echo "[claude] update: download failed, using cached" >&2 fi fi else echo "[claude] update: could not query npm registry, using cached" >&2 fi touch "\$STAMP" fi # Pick the highest installed version bin=\$(ls -1 "\$VERSIONS_DIR" 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\$' | sort -V | tail -1) if [ -z "\$bin" ] || [ ! -f "\$VERSIONS_DIR/\$bin" ]; then echo "[claude] no installed binary in \$VERSIONS_DIR. Re-run install.sh" >&2 exit 1 fi bin="\$VERSIONS_DIR/\$bin" # Self-heal: re-patch if anything outside our control swapped the binary interp=\$(LD_PRELOAD= "\$PATCHELF" --print-interpreter "\$bin" 2>/dev/null || echo unknown) if [ "\$interp" != "\$GLIBC_LD" ]; then echo "[claude] re-patching ELF interpreter (was: \$interp)" >&2 LD_PRELOAD= "\$PATCHELF" --set-interpreter "\$GLIBC_LD" "\$bin" \ || { echo "[claude] patchelf failed; cannot run \$bin" >&2; exit 1; } fi unset LD_PRELOAD exec "\$bin" "\${args[@]}" EOF chmod +x "$WRAPPER" ok "wrapper installed at $WRAPPER" # --- Native-install launcher discovery --- # Claude Code sees the binary under ~/.local/share/claude/versions, treats it as # a native install, and expects a launcher at ~/.local/bin/claude with # ~/.local/bin on PATH. Without them it prints "Native installation ... not in # your PATH" notices at startup. Set both up the way claude's own message # prescribes. The launcher points at this wrapper so every invocation still # routes through it; ~/.local/bin is appended to PATH so $PREFIX/bin stays first. mkdir -p "$HOME/.local/bin" ln -sfn "$WRAPPER" "$HOME/.local/bin/claude" if ! grep -Fq 'native-install launcher discovery' "$HOME/.bashrc" 2>/dev/null; then printf '\n# claude-code-android: native-install launcher discovery\nexport PATH="$PATH:$HOME/.local/bin"\n' >> "$HOME/.bashrc" ok "added ~/.local/bin to PATH in ~/.bashrc" else ok "PATH already includes ~/.local/bin in ~/.bashrc" fi # --- Recommended packages (Q2) --- if [ "$RECOMMENDED" = 1 ]; then info "installing recommended packages (this is the longest step)" apt-get install $APT_OPTS git gh wget jq python openssh tree proot \ termux-api proot-distro make clang file xxd htop bat fzf >/dev/null \ || fail "recommended package install failed" ok "recommended packages installed" fi # --- Verify --- hash -r 2>/dev/null || true VER="$(claude --version 2>&1)" || fail "claude --version failed: $VER" ok "claude --version: $VER" # --- Done --- cat <