v2.1.113+ broken on Termux/Android: native binary requires glibc, no JS fallback

Open 💬 65 comments Opened Apr 18, 2026 by tpdhd
💡 Likely answer: A maintainer (ant-kurt, collaborator) responded on this thread — see the highlighted reply below.

Summary

Starting with v2.1.113, Claude Code switched from a JavaScript entry point (cli.js) to a native glibc Linux binary (bin/claude.exe). This completely breaks Claude Code on Termux (Android), where:

  • process.platform reports android, not linux
  • Android's kernel rejects ET_EXEC glibc ELF binaries (unexpected e_type: 2)
  • There is no glibc on Android/Termux (it uses Bionic libc)

v2.1.112 (last JS-based version) works perfectly.

Environment

  • Device: Pixel 9, Android 14 (non-rooted)
  • Termux: latest from F-Droid
  • Node.js: v22 (Termux arm64)
  • Claude Code: v2.1.113 / v2.1.114 broken, v2.1.112 works

Steps to reproduce

# On Termux
npm install -g @anthropic-ai/claude-code@latest
claude --version

Expected: Version output
Actual:

Error: claude native binary not installed.

Either postinstall did not run (--ignore-scripts, some pnpm configs)
or the platform-native optional dependency was not downloaded
(--omit=optional).

Root cause

  1. install.cjs calls process.platform → returns android → not in PLATFORMS map → skips binary install
  2. Even force-installing @anthropic-ai/claude-code-linux-arm64 and copying the binary fails because Android's kernel rejects glibc executables:

``
error: "bin/claude.exe" has unexpected e_type: 2
``

  1. Running via patchelf or the glibc ld-linux-aarch64.so.1 loader also fails (Could not find a PHDR: broken executable?)
  2. Running via proot-distro works (proot-distro login ubuntu -- /path/to/claude --version) but is too slow for interactive use

Workaround

Pin to the last JS-based version:

npm install -g @anthropic-ai/claude-code@2.1.112

Suggested fix

One or more of:

  • Add android-arm64 platform support with a statically-linked or Bionic-compatible binary
  • Keep the JS fallback (cli.js / cli-wrapper.cjs) functional as a real fallback, not just an error message
  • Map androidlinux in platform detection (won't fix the glibc issue alone, but would help if a compatible binary is provided)

Impact

All Termux users on Android are locked out of updates. Termux is one of the few ways to run Claude Code on mobile — a non-trivial use case for developers on the go.

View original on GitHub ↗

65 Comments

cyberb · 2 months ago

Please fix it, the whole idea of terminal agent is that I do not need IDE in most of the cases now, which means I do not really need my 4 screens , which means no need for a PC. Android termux will th 5 terminals running Claude and doing everything I need is almost all I need these days.

salviz · 2 months ago

+1, hitting the same regression on Samsung S918B (Galaxy S23 Ultra) running Termux native (not proot).

Environment

  • Device: Samsung SM-S918B
  • OS: Android 16, kernel 5.15.178-android13-8
  • Termux: native arm64 (no proot)
  • Node: v24.14.1
  • npm: 11.12.1
  • claude-code: 2.1.114 from npm install -g

Reproduction confirmed

process.platform on Termux Node returns "android", not "linux":

$ node -e "console.log(process.platform, require('os').arch())"
android arm64

install.cjs then falls through the if (platform === 'linux') branch, builds platform key "android-arm64", looks it up in PLATFORMS, gets undefined, and bails with the "Unsupported platform" message — leaving only the 155 KB wrapper stub. Every subsequent claude invocation prints:

Error: claude native binary not installed.
Either postinstall did not run (--ignore-scripts, some pnpm configs)
or the platform-native optional dependency was not downloaded
(--omit=optional).
...

Real-world impact

This broke an unattended cron job at 09:15 today: npm update -g --ignore-scripts ran at 08:32, pulled v2.1.114, and the next claude invocation crashed before any session could start. The script's mid-session resume logic was useless because the session never existed.

Workaround verified end-to-end

npm uninstall -g --ignore-scripts @anthropic-ai/claude-code
npm cache clean --force
npm install -g @anthropic-ai/claude-code@2.1.112
claude --version  # → 2.1.112 (Claude Code)

Two requests for Anthropic

  1. Add an android-arm64 entry to the PLATFORMS map in install.cjs and cli-wrapper.cjs (Termux is one of the largest "Linux on phone" environments and this regression cuts it off entirely). Either ship a real Android-bionic native build, or fall back to the linux-arm64 package with a runtime check, or — at minimum — preserve the previous self-contained cli.js JS entry point as a fallback when no native binary matches.
  2. Until the above ships, please republish v2.1.112 as latest or otherwise prevent automatic updates from breaking every Termux user's unattended workflows. Right now anyone with npm update -g in their cron / Dockerfile / dotfiles bootstrap gets silently broken.

Happy to test patches against this device.

daniel-thisnow · 2 months ago

Confirming on Termux / Android 13 / aarch64

  • Termux Node v25.3.0
  • npm i -g @anthropic-ai/claude-code@2.1.114 placed a 236MB native binary at $PREFIX/lib/node_modules/@anthropic-ai/claude-code/bin/claude.exe
  • file reports: ELF 64-bit LSB executable, ARM aarch64, …, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0
  • ldd wants /lib/aarch64-linux-gnu/libc.so.6 etc. — none exist on Termux (bionic, Android linker at /system/bin/linker64). Binary refuses to exec.
  • package.json optionalDependencies only targets glibc/musl linux, darwin, win32 — no android-bionic entry.

Workaround: npm i -g @anthropic-ai/claude-code@2.1.112 (last version with "bin": { "claude": "cli.js" }) restores the pure-Node entry and works in Termux immediately.

Two requests:

  1. Until an android-arm64 / bionic target ships, could the wrapper fall back to cli-wrapper.cjs when no \PLATFORMS\ key matches, instead of leaving a naked ELF that can't exec? The wrapper file is already shipped in the package but never invoked when the arch genuinely isn't listed.
  2. A documented "last-known-good" pin for Android users would unblock the downgrade path without archaeology.
daniel-thisnow · 2 months ago

Follow-up: 2.1.114 has the same regression — and the in-process auto-updater is actively reverting manual pins.

Same Termux/Android 13/aarch64 device as my previous comment.

2.1.114 confirmed broken the same way as 2.1.113

  • npm i -g @anthropic-ai/claude-code@2.1.114bin/claude.exe is now just the 155-byte error stub (no android-arm64 in optionalDependencies, so postinstall bails). Previous cli.js is gone.
  • Every fresh claude invocation dies with "Error: claude native binary not installed."

New failure mode: the in-process auto-updater silently clobbers a manual downgrade

Inside a running claude session:

  1. npm i -g @anthropic-ai/claude-code@2.1.112 — confirmed "version": "2.1.112", cli.js restored, package.json timestamp fresh.
  2. A few seconds later, re-checked the same package.json: "version": "2.1.114", cli.js gone, directory mtime updated.

No other process was touching the tree — only the running claude. The built-in updater is on a timer and re-fetches latest, overwriting the pinned install while the user is mid-session. So for Termux users the current state is:

  • 2.1.113+ on-disk = broken.
  • Downgrading to 2.1.112 lasts only until the next updater tick, which can happen within the same session you downgraded from.

What actually survives on Termux right now

Belt-and-braces recipe that held:

DISABLE_AUTOUPDATER=1 npm i -g @anthropic-ai/claude-code@2.1.112
chmod -R a-w \$PREFIX/lib/node_modules/@anthropic-ai/claude-code/
export DISABLE_AUTOUPDATER=1   # also in ~/.zshrc / ~/.bashrc

And in ~/.claude/settings.json:

"env": { "DISABLE_AUTOUPDATER": "1" }

The read-only chmod is load-bearing — without it, the updater's write silently wins.

Concrete asks, restated

  1. Make the updater honor DISABLE_AUTOUPDATER=1 and skip auto-update when the current install's platform key isn't in PLATFORMS (i.e., don't auto-"upgrade" a user into a broken state on platforms the release doesn't target).
  2. Restore cli-wrapper.cjs as an automatic fallback when no PLATFORMS entry matches the host. Shipping the file and never using it is a footgun.
  3. Until (1)+(2) land, please consider tagging 2.1.112 as npm dist-tag add @anthropic-ai/claude-code@2.1.112 termux (or similar) so the downgrade command is documentable and doesn't rot.

Happy to test any patch on the same device.

ferrumclaudepilgrim · 2 months ago

For anyone landing here looking for a ready recovery path, I maintain claude-code-android specifically for this install target and shipped v2.7.0 last night with the pinned install plus the auto-updater defense.

@daniel-thisnow nailed the auto-updater issue above. Credit to him for the chmod finding. The v2.7.0 install script bundles his defense into an idempotent fresh-install / recover-in-place flow so users can go from the broken error to a working claude in one command.

Two install paths in the repo:

  • Path A (native Termux): pinned to 2.1.112, DISABLE_AUTOUPDATER=1 set in env and settings.json, install dir chmod a-w. Works today.
  • Path B (proot-distro Ubuntu): unaffected by this regression because process.platform reports linux inside the guest. Heavier setup, but immune to platform-native binary drift upstream.

Repo: https://github.com/ferrumclaudepilgrim/claude-code-android

I will keep it current as the upstream situation evolves.

rafaelbressan · 2 months ago

Confirming on Samsung A55, Termux native (no proot), aarch64

Same claude native binary not installed on npm i -g @anthropic-ai/claude-code@latest. My setup:

  • Samsung A55, Termux native (no proot-distro)
  • Node v24.14.1
  • process.platform returns android, arch arm64
  • npm root at /data/data/com.termux/files/usr/lib/node_modules

cliwrapper.cjs is in the package but there's no fallback. Running it directly just surfaces the same "unsupported platform" error from install.cjs. The file ships but never runs, which is the footgun the OP called out.

What's holding for me

Pinned to 2.1.112 (last version with a working cli.js) with three layers of lock against the autoupdater silently winning:

npm uninstall -g @anthropic-ai/claude-code
DISABLE_AUTOUPDATER=1 npm i -g @anthropic-ai/claude-code@2.1.112 --ignore-scripts
chmod -R a-w $PREFIX/lib/node_modules/@anthropic-ai/claude-code/

In ~/.bashrc:

export DISABLE_AUTOUPDATER=1
alias claude='node /data/data/com.termux/files/usr/lib/node_modules/@anthropic-ai/claude-code/cli.js'

In ~/.claude/settings.json:

{ "env": { "DISABLE_AUTOUPDATER": "1" } }

Without the read-only lock chmod -R a-w , the updater's write can still win against the env var in some paths and silently upgrade you back into the broken state.

Echoing the asks

  1. Honor DISABLE_AUTOUPDATER=1 everywhere, and skip auto-update when the current platform key isn't in PLATFORMS. Auto-upgrading users into a broken state on unsupported platforms is worse than not updating them.
  2. Restore cliwrapper.cjs as a real fallback when PLATFORMS has no match. The file is already in the package. Making it a trap instead of a safety net is reversible.
  3. Until 1 and 2 ship, an npm dist-tag like @anthropic-ai/claude-code@termux pointing at 2.1.112 would make the workaround documentable and stop the downgrade command from rotting.

Why this matters

Termux is how a lot of people run dev on Android. For me it's the phone side of an SSH-into my Debian workflow, with local Claude Code as the fallback when I'm offline or the server's not reachable or when i need to tweak Termux with Claude. Losing that breaks the "check on the agent loop from anywhere" use case, which is exactly what Claude Code agentic workflows are designed for.

Happy to test fixes on the android-arm64 path.

aryehbeitz · 2 months ago

Deep-dive + proposed minimal patches

Inspected the actual 2.1.114 npm tarball. The failure path is small and easy to fix even without shipping an Android binary — three compounding bugs each have a ~5-line fix:

Bug 1 — install.cjs silently no-ops on Android, leaves a misleading stub

install.cjs line 127-137 in 2.1.114:

function main() {
  const platformKey = getPlatformKey()
  const info = PLATFORMS[platformKey]

  if (!info) {
    console.error(
      `[${WRAPPER_NAME} postinstall] Unsupported platform: ${process.platform} ${arch()}`,
    )
    console.error(`  Supported: ${Object.keys(PLATFORMS).join(', ')}`)
    return   // ← silent success; exit code 0
  }
  ...
}

On Termux, process.platform === 'android', so getPlatformKey() returns 'android-arm64', PLATFORMS['android-arm64'] is undefined, and main() returns with exit code 0. npm reports a successful install. The stub bin/claude.exe (a 500-byte shell script) stays in place, and the user only discovers breakage at first claude invocation — with an error message that points them in the wrong direction (see Bug 2).

Bug 2 — the stub error message gives misleading advice on Termux

bin/claude.exe stub content:

echo "Error: claude native binary not installed." >&2
...
echo "Either postinstall did not run (--ignore-scripts, some pnpm configs)"
echo "or the platform-native optional dependency was not downloaded"
echo "(--omit=optional)."
...
echo "Run the postinstall manually:"
echo "  node node_modules/@anthropic-ai/claude-code/install.cjs"
echo "Or reinstall without --ignore-scripts / --omit=optional."

On Termux, none of this advice applies — postinstall ran, optional deps downloaded, the binary was just skipped because the platform isn't in PLATFORMS. Users follow the advice, reinstall, hit the same error, and conclude it's just broken.

Bug 3 — auto-updater re-breaks the pinned install

Even after npm install -g @anthropic-ai/claude-code@2.1.112 --force, something in the claude CLI auto-upgrades to @latest on next launch, silently re-bricking the install. Users hit the native-binary error a second time and don't understand why. The only reliable fix on Termux right now is also setting DISABLE_AUTOUPDATER=1 in settings/env — which isn't documented for this failure mode.

Proposed minimal patch (no Android binary required)

Just detecting Android in the two shared PLATFORMS/getPlatformKey() code blocks and failing loudly with correct advice would save every Termux user from this confusing debug loop. Proposed diff against install.cjs:

 function main() {
   const platformKey = getPlatformKey()
+
+  // Termux/Android: no compatible native binary exists (Bionic libc, not glibc).
+  // Fail loudly at install time with a working workaround so users don't hit the
+  // misleading "native binary not installed" stub at runtime.
+  if (process.platform === 'android') {
+    console.error(
+      `[${WRAPPER_NAME} postinstall] Android/Termux is not supported by the native binary (Bionic libc).`,
+    )
+    console.error(
+      '  Workaround: pin to the last JS-based release:',
+    )
+    console.error(
+      `    npm install -g ${WRAPPER_NAME}@2.1.112`,
+    )
+    console.error(
+      '  Also set DISABLE_AUTOUPDATER=1 in your environment to prevent silent',
+    )
+    console.error(
+      '  re-upgrade. Tracking issue: anthropics/claude-code#50270',
+    )
+    process.exitCode = 1
+    return
+  }
+
   const info = PLATFORMS[platformKey]
   ...
 }

Same block should be added to cli-wrapper.cjs inside getBinaryPath() for the --ignore-scripts path.

Broader fix (auto-updater)

The auto-updater inside the CLI should skip itself on process.platform === 'android' (or at minimum pin to @2.1.112 instead of @latest) until an Android binary is shipped. Right now it actively breaks working installs every few hours — arguably worse than the install bug itself.

Ideal fix

Ship @anthropic-ai/claude-code-android-arm64 built against Bionic (or statically linked). Termux has ~1M+ installs; this is a non-trivial userbase. Happy to test an alpha build on Pixel 9 / Android 14 if it'd help.

---

Environment: Termux on Android 14 (aarch64), Node 22, npm 10.

myudak · 2 months ago

how is this issue still open bruh

oespinozai · 2 months ago

Seems like we are being forced to use the app???

alex-rosenberg35 · 2 months ago

@aryehbeitz is spot-on. It is bewildering for those of us who have been using Claude Code on Termux to suddenly find it bricked with the misleading stub error message after the updater silently broke it.

I am happy to report Codex (@mmmbuto/codex-cli-termux) continues to work on Termux.

Anthropic, get this fixed. Termux is popular and heavily used by those who need it. Even the old version on Google Play shows it has 10M+ downloads.

kingpanther13 · 2 months ago

Confirming on 2.1.116 / Termux / Android (aarch64-linux-android), kernel 5.15.178-android13-8. Same regression — npm update bumped from 2.1.112 to 2.1.116, install.cjs rejected with Unsupported platform: android arm64, bin/claude.exe is the 500-byte stub, every claude invocation dies.

Additional data point on the musl fallback: I tried npm install -g @anthropic-ai/claude-code-linux-arm64-musl@2.1.116 --force to sidestep install.cjs entirely. Install succeeds, but the binary itself refuses to exec:

$ file .../claude-code-linux-arm64-musl/claude
ELF 64-bit LSB executable, ARM aarch64, ..., dynamically linked,
  interpreter /lib/ld-musl-aarch64.so.1, ...

$ .../claude
bash: .../claude: cannot execute: required file not found

The musl variant isn't a viable escape hatch on Termux either — it's dynamically linked against musl's loader, which Android's Bionic doesn't provide (unlike truly-static musl binaries like ruff/uv, which do work fine on Termux). Either a genuine \android-arm64\ build or a statically-linked musl binary would unbreak this.

Rollback to 2.1.112 works as described above. +1 on restoring a \cli.js\ / \cli-wrapper.cjs\ JS fallback when no \PLATFORMS\ key matches.

sb-dev · 2 months ago

While waiting for a fix:

npm uninstall -g @anthropic-ai/claude-code
npm install -g @anthropic-ai/claude-code@2.1.112

Then update ~/.claude/settings.json with:

{
  "autoUpdates": false
}
ant-kurt collaborator · 2 months ago

Claude Code 2.1.113 has switched to a native executable (after npm deprecation was announced in January), which drops support for some platforms. We may add support for Android in the future - this requires either a bun target for android-arm64 or static musl.

You can pin to the last JS bundle release https://www.npmjs.com/package/@anthropic-ai/claude-code/v/2.1.112 to continue using Claude Code for now.

ilfelice · 2 months ago

+1 from a Haiku OS user. Same root cause as the Termux reports: process.platform returns haiku, which isn't in install.cjs's PLATFORMS map, so the postinstall skips the native binary download entirely and the npm wrapper fails at launch with "claude native binary not installed."

I've been running Claude Code happily on Haiku for months — it worked because the pre-2.1.113 CLI was plain Node.js and didn't care what platform it ran on. The switch to a platform-native binary wasn't aimed at platforms like mine, but it hard-fails on them rather than degrading gracefully.

I'm currently pinned to 2.1.112, which means I'm missing the fixes from the April 23 post-mortem (notably the verbosity system prompt reverted in 2.1.116). Staying pinned isn't sustainable long-term.

The ask here isn't "officially support Haiku" — I understand that's a real engineering commitment. The ask is much smaller: restore a JS fallback for platforms where no prebuilt binary exists. When process.platform isn't in the PLATFORMS map, fall back to the bundled cli.js path that worked fine through 2.1.112, possibly behind a flag or with a warning that the platform is unsupported. This would unblock Haiku, Termux/Android, FreeBSD, and any future platform without requiring Anthropic to maintain binaries for them.

Happy to test patches on Haiku if that would help.

ScientificProgrammer · 2 months ago

Workaround refinement not yet in this thread: npm install -g @anthropic-ai/claude-code@stable works identically to @2.1.112 today, and is more durable than version-pinning. As of 2026-04-25, the stable dist-tag still points to 2.1.112:

$ npm view @anthropic-ai/claude-code dist-tags
{ stable: '2.1.112', next: '2.1.119', latest: '2.1.119' }

Anthropic kept the last pure-JS release as @stable rather than promoting any native-binary version. Pinning to @stable is preferable to @2.1.112 because:

  • If 2.1.112 ever gets unpublished, @stable keeps working
  • If a 2.1.112.x patch backport ships, @stable picks it up automatically
  • It documents intent ("last known good") rather than a magic version

Combine with "autoUpdates": false in ~/.claude/settings.json (per @sb-dev) to defend against the in-session auto-updater silently re-bricking the install, and this is a one-shot, persistent fix:

# Wipe leftover native-binary opt-deps from prior failed installs (240MB+ each)
NM=$(npm root -g)
for V in linux-arm64 linux-arm64-musl linux-x64 linux-x64-musl darwin-arm64 darwin-x64 win32-arm64 win32-x64; do
  rm -rf "${NM}/@anthropic-ai/claude-code-${V}"
done

npm install -g @anthropic-ai/claude-code@stable

# Lock auto-updates without clobbering existing settings.
jq '. + {autoUpdates: false}' ~/.claude/settings.json > /tmp/s.json && mv /tmp/s.json ~/.claude/settings.json

Confirmed working 2026-04-25 on Samsung S26 Ultra (Android 16, kernel 6.12.30-android16, Termux 0.118.3, Node 24.14.1, npm 11.13.0) — adding to the device-confirmation list since Android 16 / S26 hardware hasn't been reported yet in this thread.

cowile · 2 months ago

Same issue on OpenBSD x64. The native binary is a glibc Linux ELF that can't run on OpenBSD (no Linux emulation layer in OpenBSD 7.x). I'm currently pinning to 2.1.109

The fix request is the same: restore a JS fallback path for platforms not supporting a native binary. I'm fine to eat the performance cost for more cross-platform support. I understand you can't ship a binary for everything.

oespinozai · 2 months ago

DeepseekPro4 is another beast. I've switched.

On Fri, May 1, 2026 at 7:11 PM cowile @.***> wrote:

cowile left a comment (anthropics/claude-code#50270) <https://github.com/anthropics/claude-code/issues/50270#issuecomment-4360850037> Same issue on OpenBSD x64. The native binary is a glibc Linux ELF that can't run on OpenBSD (no Linux emulation layer in OpenBSD 7.x). I'm currently pinning to 2.1.109 The fix request is the same: restore a JS fallback path for platforms not supporting a native binary. I'm fine to eat the performance cost for more cross-platform support. I understand you can't ship a binary for everything. — Reply to this email directly, view it on GitHub <https://github.com/anthropics/claude-code/issues/50270#issuecomment-4360850037>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AAOEA43W4IZOBJUVUDTKGDT4YTSD7AVCNFSM6AAAAACX5VXAI6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DGNRQHA2TAMBTG4> . You are receiving this because you commented.Message ID: @.***>

--
Oscar Espinoza
oscarespinoza.co.uk <http://oscarespinoza.co.uk>

nick-the-dev · 2 months ago

+1

noezra · 2 months ago

I hope this gets support.

gtbuchanan · 2 months ago

Run the current native binary on Termux (alternative to pinning to 2.1.112)

Patchelf the linux-arm64 binary's ELF interpreter to glibc-runner's ld.so, exec it directly with LD_PRELOAD cleared so termux-exec doesn't crash it on startup, then re-apply LD_PRELOAD via claude's settings env so subprocess shebang resolution still works.

Updated 2026-06-01 to keep LD_PRELOAD (so #!/usr/bin/env shebangs in pnpm/npx and your own scripts keep resolving) and fix the bundled grep/find/rg that the re-injected LD_PRELOAD otherwise breaks — via a length-preserving binary patch plus a compiled launcher wrapper. This avoids the lossy alternative of dropping LD_PRELOAD entirely, which silently breaks every #!/usr/bin/env-shebang tool.

One-time setup:

# glibc-runner ships a working glibc ld.so at $PREFIX/glibc/lib/;
# patchelf-glibc rewrites ELF interpreter strings. Both come from the
# glibc-packages termux repo, which the glibc-repo package enables.
# jq parses the upstream release manifest for checksum verification.
# clang compiles the launcher wrapper (must be a real ELF, not a script).
pkg install -y clang jq glibc-repo
pkg update
pkg install -y glibc-runner patchelf-glibc

# Put the wrapper directory on PATH if it isn't already.
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

Install / re-run on each version bump (set VERSION to the desired release):

#!/data/data/com.termux/files/usr/bin/bash
set -euo pipefail

VERSION=2.1.156
BINARY=~/.local/share/claude-code/claude
WRAPPER=~/.local/bin/claude
DL=https://downloads.claude.ai/claude-code-releases/$VERSION
GLIBC_PREFIX=/data/data/com.termux/files/usr/glibc
PATCHELF=$GLIBC_PREFIX/bin/patchelf
GLIBC_LD=$GLIBC_PREFIX/lib/ld-linux-aarch64.so.1

mkdir -p "$(dirname "$BINARY")" "$(dirname "$WRAPPER")"

# Patch a tempfile, then rename in. Renaming over the running binary is
# safe; patching it in place would fail with "Text file busy".
tmp=$(mktemp); patched=$tmp.patched
trap 'rm -f "$tmp" "$patched"' EXIT
curl -fsSL "$DL/linux-arm64/claude" -o "$tmp"

# Verify against the published manifest checksum.
expected=$(curl -fsSL "$DL/manifest.json" | jq -er '.platforms["linux-arm64"].checksum')
actual=$(sha256sum "$tmp" | cut -d' ' -f1)
if [ "$actual" != "$expected" ]; then
  echo "checksum mismatch: $actual != $expected" >&2
  exit 1
fi

# Set the ELF interpreter to glibc-runner's ld.so so the kernel can exec
# the binary directly — needed because Claude's bundled grep/find/rg
# re-exec it with argv[0]=ugrep/bfs/rg, and argv[0] only survives a
# kernel-direct exec (`grun` chains via ld.so and breaks it). LD_PRELOAD=
# stops termux-exec from crashing patchelf: its DT_NEEDED `libc.so` is a
# linker script under $PREFIX/glibc/lib, not an ELF.
LD_PRELOAD= "$PATCHELF" --output "$patched" --set-interpreter "$GLIBC_LD" "$tmp"

# Blank the subprocess CLAUDE_CODE_EXECPATH. The grep/find/rg functions
# prefer it for their re-exec; with LD_PRELOAD re-injected (below) that
# re-exec inherits the bionic preload and crashes the glibc binary.
# Emptying it makes them fall back to the wrapper, which clears
# LD_PRELOAD. Replacing `process.execPath` with `""` plus trailing spaces
# keeps the file length fixed; backreferences keep the anchor unique
# across Bun's per-release identifier churn.
python3 - "$patched" <<'PY'
import sys, re
p = sys.argv[1]
data = open(p, "rb").read()
ident = rb"[A-Za-z_$][A-Za-z0-9_$]*"
anchor = re.compile(rb"((" + ident + rb")\[" + ident + rb"\]=)process\.execPath(,(" + ident + rb")\)\2\.TMUX=\4)")
matches = anchor.findall(data)
if len(matches) != 1:
    sys.exit(f"execPath patch: expected 1 match, got {len(matches)} (Bun output changed?)")
replacement = b'""' + b" " * 14   # 16 bytes, same width as "process.execPath"
open(p, "wb").write(anchor.sub(lambda m: m.group(1) + replacement + m.group(3), data))
PY

chmod +x "$patched"
mv "$patched" "$BINARY"

# Compile the launcher wrapper. It must be a real ELF, not a #! script:
# the grep re-exec falls back to it, and a script wrapper would let the
# kernel discard argv[0]=ugrep, whereas execv() preserves it. Being a
# bionic binary, termux-exec loads into it fine; it then clears
# LD_PRELOAD before exec'ing the glibc claude binary so it starts clean.
cc -O2 -DBINARY="\"$BINARY\"" -o "$WRAPPER" -xc - <<'EOF'
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char **argv) {
  (void)unsetenv("LD_PRELOAD");
  execv(BINARY, argv);
  fprintf(stderr, "claude wrapper: execv %s failed: %s\n", BINARY, strerror(errno));
  return 127;
}
EOF

Add to ~/.claude/settings.json:

{
  "autoUpdates": false,
  "env": {
    "LD_PRELOAD": "/data/data/com.termux/files/usr/lib/libtermux-exec-ld-preload.so"
  }
}

autoUpdates: false stops the in-session updater from re-downloading the broken bundle. The LD_PRELOAD entry is applied after the dynamic linker finishes with claude (so it doesn't crash the main process), but is in process.env when claude spawns subprocesses, so #!/usr/bin/env foo shebangs in tools like pnpm and npx resolve correctly inside the Bash tool.

Chezmoi-managed version with Renovate-pinned releases: gtbuchanan/dotfiles.

lcdcode · 1 month ago

Can confirm the gtbuchanan method works!

One small note for anyone like me that uses fish instead of bash: replace the .bashrc step with fish_add_path $HOME/.local/bin
Different methods for different shells...

Thank you, @gtbuchanan!

flipphoneguy · 1 month ago

Nice! Thanks for this!!

Here's a script that automates it a bit better for you. You can just run this every time you wanna check for updates and update. It'll handle all the first time setup and conflicts for you.

You can save this to a file and run it whenever you wanna try updating

  • Checks if dependencies are installed and installs them for you after you confirm - if missing.
  • Checks if new version is available (instead of hardcoded version)
  • Ensures .claude/settings.json is configured correctly for this (adds the necessary values. Doesn't wipe it)
  • automatically detect your shell and add wrapper to PATH in your shells rc
  • Checks if old npm is still installed and suggests to either rename so it doesn't conflict, uninstall it or just keep as is.
#!/data/data/com.termux/files/usr/bin/bash
set -euo pipefail

BINARY=~/.local/share/claude-code/claude
WRAPPER=~/.local/bin/claude
BASE_URL=https://downloads.claude.ai/claude-code-releases
GLIBC_LD=/data/data/com.termux/files/usr/glibc/lib/ld-linux-aarch64.so.1
SETTINGS=~/.claude/settings.json

# ---------------------------------------------------------------------------
# Dependencies
# ---------------------------------------------------------------------------

DEPS=(curl jq glibc-runner patchelf-glibc)
MISSING=()

for dep in "${DEPS[@]}"; do
  if ! dpkg -s "$dep" &>/dev/null; then
    MISSING+=("$dep")
  fi
done

# glibc-runner and patchelf-glibc come from the glibc-packages repo,
# which the glibc-repo package enables. Only needed if either is missing.
if ! dpkg -s glibc-repo &>/dev/null; then
  need_repo=false
  for dep in "${MISSING[@]}"; do
    if [[ "$dep" == "glibc-runner" || "$dep" == "patchelf-glibc" ]]; then
      need_repo=true
      break
    fi
  done
  if $need_repo; then
    MISSING=(glibc-repo "${MISSING[@]}")
  fi
fi

if [ ${#MISSING[@]} -gt 0 ]; then
  echo "The following packages are required but not installed:"
  echo ""
  for dep in "${MISSING[@]}"; do
    echo "  - $dep"
  done
  echo ""
  read -rp "Install them now? [Y/n] " ans
  if [[ "$ans" =~ ^[Nn] ]]; then
    echo "Cannot continue without dependencies." >&2
    exit 1
  fi

  # If glibc-repo is in the list, install it first and refresh the
  # package index so glibc-runner / patchelf-glibc become available.
  if [[ " ${MISSING[*]} " =~ " glibc-repo " ]]; then
    pkg install -y glibc-repo
    apt update
    remaining=()
    for dep in "${MISSING[@]}"; do
      [ "$dep" != "glibc-repo" ] && remaining+=("$dep")
    done
    MISSING=("${remaining[@]}")
  fi

  if [ ${#MISSING[@]} -gt 0 ]; then
    pkg install -y "${MISSING[@]}"
  fi

  echo ""
  echo "Dependencies installed."
fi

# ---------------------------------------------------------------------------
# Version check
# ---------------------------------------------------------------------------

LATEST=$(curl -fsSL "$BASE_URL/latest/manifest.json" | jq -er '.version')
CURRENT=$("$WRAPPER" --version 2>/dev/null | awk '{print $1}' || echo "none")

if [ "$CURRENT" = "$LATEST" ] && [ $# -eq 0 ]; then
  echo "Already on latest: $CURRENT"
  exit 0
fi

VERSION="${1:-$LATEST}"
DL="$BASE_URL/$VERSION"

if [ "$CURRENT" = "none" ]; then
  echo "Installing Claude Code $VERSION ..."
else
  echo "Updating: $CURRENT -> $VERSION"
fi

# ---------------------------------------------------------------------------
# Download & verify
# ---------------------------------------------------------------------------

mkdir -p "$(dirname "$BINARY")" "$(dirname "$WRAPPER")"

TMPBIN=$(mktemp)
trap 'rm -f "$TMPBIN"' EXIT INT TERM

curl -fSL "$DL/linux-arm64/claude" -o "$TMPBIN"

expected=$(curl -fsSL "$DL/manifest.json" | jq -er '.platforms["linux-arm64"].checksum')
actual=$(sha256sum "$TMPBIN" | cut -d' ' -f1)
if [ "$actual" != "$expected" ]; then
  echo "Checksum mismatch: $actual != $expected" >&2
  exit 1
fi

mv "$TMPBIN" "$BINARY"
trap - EXIT INT TERM
chmod +x "$BINARY"

# ---------------------------------------------------------------------------
# Patchelf
# ---------------------------------------------------------------------------

# Patchelf the binary's ELF interpreter to glibc-runner's ld.so so
# the kernel can exec it directly. Required because Claude's embedded
# grep/find re-execs via `exec -a ugrep $CLAUDE_CODE_EXECPATH`, and
# argv[0] preservation only survives a kernel-direct exec. Running
# under `grun` instead leaves $CLAUDE_CODE_EXECPATH pointing at
# ld.so, which mis-parses ugrep's `-G` as the executable.
#
# LD_PRELOAD= here keeps termux-exec from crashing patchelf itself.
# libtermux-exec-ld-preload.so has DT_NEEDED for unversioned `libc.so`,
# and in $PREFIX/glibc/lib that path is a static-linker text script,
# so the dynamic linker errors out with "invalid ELF header" on it.
LD_PRELOAD= patchelf --set-interpreter "$GLIBC_LD" "$BINARY"

# ---------------------------------------------------------------------------
# Wrapper script
# ---------------------------------------------------------------------------

# Wrapper clears LD_PRELOAD for the same reason. Claude's binary is
# also glibc and would crash on startup with termux-exec preloaded.
cat > "$WRAPPER" <<EOF
#!/data/data/com.termux/files/usr/bin/bash
unset LD_PRELOAD
exec "$BINARY" "\$@"
EOF
chmod +x "$WRAPPER"

# ---------------------------------------------------------------------------
# Settings
# ---------------------------------------------------------------------------

mkdir -p "$(dirname "$SETTINGS")"

LD_PRELOAD_VAL="/data/data/com.termux/files/usr/lib/libtermux-exec-ld-preload.so"

if [ ! -f "$SETTINGS" ]; then
  cat > "$SETTINGS" <<SETTINGSEOF
{
  "autoUpdates": false,
  "env": {
    "LD_PRELOAD": "$LD_PRELOAD_VAL"
  }
}
SETTINGSEOF
  echo "Created $SETTINGS"
elif ! jq empty "$SETTINGS" 2>/dev/null; then
  echo "Warning: $SETTINGS contains invalid JSON — skipping settings update."
  echo "Add these fields manually:"
  echo "  \"autoUpdates\": false"
  echo "  \"env\": { \"LD_PRELOAD\": \"$LD_PRELOAD_VAL\" }"
else
  changed=false
  tmp=$(mktemp)
  cp "$SETTINGS" "$tmp"

  if [ "$(jq '.autoUpdates' "$tmp")" != "false" ]; then
    jq '.autoUpdates = false' "$tmp" > "${tmp}.2" && mv "${tmp}.2" "$tmp"
    changed=true
  fi

  if [ "$(jq -r '.env.LD_PRELOAD // empty' "$tmp")" != "$LD_PRELOAD_VAL" ]; then
    jq --arg val "$LD_PRELOAD_VAL" '.env.LD_PRELOAD = $val' "$tmp" > "${tmp}.2" && mv "${tmp}.2" "$tmp"
    changed=true
  fi

  if [ "$changed" = true ]; then
    mv "$tmp" "$SETTINGS"
    echo "Updated $SETTINGS (set autoUpdates / LD_PRELOAD)"
  else
    rm -f "$tmp"
    echo "Settings already configured."
  fi
fi

# ---------------------------------------------------------------------------
# Shell RC — ensure ~/.local/bin is on PATH
# ---------------------------------------------------------------------------

WRAPPER_DIR="$(dirname "$WRAPPER")"
PATH_LINE='export PATH="$HOME/.local/bin:$PATH"'

add_to_rc() {
  local rc="$1"
  if [ -f "$rc" ] && grep -qF '/.local/bin' "$rc"; then
    echo "PATH already configured in $rc"
  else
    echo "" >> "$rc"
    echo "$PATH_LINE" >> "$rc"
    echo "Added ~/.local/bin to PATH in $rc"
    echo "Run 'source $rc' or restart your shell to apply."
  fi
}

case "$(basename "$SHELL")" in
  zsh)  add_to_rc ~/.zshrc ;;
  bash) add_to_rc ~/.bashrc ;;
  *)
    if [ -f ~/.profile ]; then
      add_to_rc ~/.profile
    else
      echo ""
      echo "Could not detect your shell RC file."
      echo "Add this line manually:"
      echo ""
      echo "  $PATH_LINE"
    fi
    ;;
esac

# Ensure the wrapper is findable for the npm-detection step below.
case ":$PATH:" in
  *:"$WRAPPER_DIR":*) ;;
  *) export PATH="$WRAPPER_DIR:$PATH" ;;
esac

# ---------------------------------------------------------------------------
# Check for old npm-installed Claude Code
# ---------------------------------------------------------------------------

NPM_CLAUDE=""
while IFS= read -r p; do
  real="$(readlink -f "$p" 2>/dev/null || echo "$p")"
  if [ "$real" != "$(readlink -f "$WRAPPER" 2>/dev/null)" ] && \
     [ "$real" != "$(readlink -f "$BINARY" 2>/dev/null)" ]; then
    NPM_CLAUDE="$p"
    break
  fi
done < <(which -a claude 2>/dev/null || true)

if [ -n "$NPM_CLAUDE" ]; then
  NPM_DIR="$(dirname "$NPM_CLAUDE")"
  echo ""
  echo "=========================================="
  echo "  Old Claude Code installation detected"
  echo "=========================================="
  echo ""
  echo "  Location: $NPM_CLAUDE"
  echo ""
  echo "  The native binary has been installed, but the old"
  echo "  version is still on your PATH and may take priority"
  echo "  or cause confusion."
  echo ""
  echo "  What would you like to do?"
  echo ""
  echo "  1) Rename it  — keep the old binary under a different name"
  echo "  2) Uninstall  — remove the npm package entirely"
  echo "  3) Do nothing — leave it as-is"
  echo ""
  read -rp "  Choose [1/2/3]: " choice

  case "$choice" in
    1)
      read -rp "  Enter new name for the old binary (e.g. claudo): " newname
      if [ -z "$newname" ]; then
        echo "  No name entered, skipping."
      elif [ -e "$NPM_DIR/$newname" ]; then
        echo "  $NPM_DIR/$newname already exists, skipping."
      else
        mv "$NPM_CLAUDE" "$NPM_DIR/$newname"
        echo ""
        echo "  Renamed: $NPM_CLAUDE -> $NPM_DIR/$newname"
        echo "  You can still use the old version as '$newname'."
      fi
      ;;
    2)
      echo ""
      echo "  Uninstalling npm Claude Code ..."
      npm uninstall -g @anthropic-ai/claude-code
      echo "  Done."
      ;;
    3)
      echo ""
      echo "  Leaving old installation in place."
      echo ""
      echo "  Note: both versions respond to the 'claude' command."
      echo "  Which one runs depends on PATH order. Currently:"
      echo "    $(which claude 2>/dev/null || echo '(not found)')"
      echo "  If that's not the native version, reorder your PATH"
      echo "  so that ~/.local/bin comes before $NPM_DIR."
      ;;
    *)
      echo "  Invalid choice, skipping."
      ;;
  esac
fi

# ---------------------------------------------------------------------------
# Done
# ---------------------------------------------------------------------------

echo ""
echo "Claude Code $VERSION is ready. Run 'claude' to start."
cyberb · 1 month ago

Great script, one problem though, Claude now complains that grep does not properly work and if I ask it fix it it starts talking about it's internal tools and I am afraid to break it.


The shell grep/rg tooling is corrupted in this environment. Let me use the Read tool directly, which is reliable

npm was fine.

kingpanther13 · 1 month ago

Ran the script on Termux (Android, aarch64, glibc-runner already set up). Had to deviate at several points to get a working install + avoid the grep break @cyberb is describing — and to recover old sessions from the npm version.

The grep breakage: it's the settings.json block that sets env.LD_PRELOAD = libtermux-exec-ld-preload.so. The wrapper correctly unset LD_PRELOAD for the main claude process, but the settings env reinjects the bionic preload for everything claude spawns — including its own exec -a ugrep $CLAUDE_CODE_EXECPATH re-exec for the bundled grep. Bionic preload has DT_NEEDED libc.so (unversioned); the glibc child's ld.so resolves that to $PREFIX/glibc/lib/libc.so which is a text linker script, not an ELF → "invalid ELF header" → Grep/Find tools die. Removing the env.LD_PRELOAD from settings entirely fixes it; the wrapper's unset is sufficient. SessionStart hooks still work fine because /bin/sh doesn't need the preload for normal operation.

Old npm sessions disappear from /resume — and cd && claude --resume <id> errors "different directory". Root cause: /home is a bind mount of /data/data/com.termux/files/home (same inode), not a symlink. The old npm claude (bionic + libtermux-exec preload) had getcwd intercepted to return the long canonical path. The new glibc claude has no such preload, so getcwd returns the kernel-tracked /home. Slugs and recorded cwd fields don't line up. Migrating the old jsonls fixes it:

  • Rewrite every cwd field from /data/data/com.termux/files/home(/...) to literally /home (flat — not /home/subdir, otherwise resume cwd-check fails when launching from $HOME).
  • Copy to ~/.claude/projects/-home/.
  • Use sed, not jq. jq re-serializes the whole line; even key-order/unicode-escape drift breaks the cryptographic signatures on extended-thinking blocks and the API rejects on resume with 400 "thinking or redacted_thinking blocks in the latest assistant message cannot be modified". A targeted sed -E 's|"cwd":"/data/data/com\.termux/files/home(/[^"]*)?"|"cwd":"/home"|g' keeps everything else byte-identical.
  • touch -r <src> <dst> after each copy so file mtimes stay accurate (otherwise /resume's sort-by-recency shows every old session as 1 minute old).

Other patches needed:

  1. Version URL is wrong (line ~85). curl "$BASE_URL/latest/manifest.json" 404s. Anthropic's actual endpoint is $BASE_URL/latest (plaintext version), then $BASE_URL/$VERSION/manifest.json. The official install.sh does it this way.
  1. patchelf not on PATH (line 129). patchelf-glibc installs the binary at $PREFIX/glibc/bin/patchelf, not anywhere default PATH will find. The bare patchelf call fails "command not found". Use the full path.
  1. Mirror prompt stall. On a Termux install without $PREFIX/etc/termux/chosen_mirrors set, pkg install glibc-repo silently sits at "No mirror or mirror group selected" forever waiting for stdin. Pin before running: ln -sf $PREFIX/etc/termux/mirrors/default $PREFIX/etc/termux/chosen_mirrors.
  1. Android lmkd SIGKILLs the parent session during the heavy install. If you run the script from inside a claude session, the pkg install glibc-* step's memory spike can take the whole process tree out via signal 9. Detach with setsid nohup bash ~/update-claude.sh </dev/null >log 2>&1 & disown or just run it in a plain Termux shell outside claude.

(Once these are all in, 2.1.156 works end-to-end with Grep/Find/SessionStart functional and old npm sessions visible.)

flipphoneguy · 1 month ago

@kingpanther13 first of all thanks for the insight and testing you've made. I'm in middle of fixing the things you mentioned.

Some of the stuff you mentioned I never had in any of my phone's termux. You seem to be using an old version of the app or possibly from the play store...

Regarding the main issue: grep and find not working, your solution is definitely the best so far, but I hate the idea of not having all my scripts work in Claude (shebangs), and I also don't know what else termux LD_PRELOAD provides.

The ideal would be (actually the ideal would be to have Claude support termux or switch to codex ;) but practically)
To have Claude started without LD_PRELOAD and instead of having it loaded into all it's child processes, we should load it only when it calls the run command tool (which I believe or hope it's internal grep and find skip). Setting it as an env variable in it's hooks didn't help.

Honestly, I don't enjoy wasting time with some random stupid hook API to get something this simple working. I prefer actually doing useful interesting coding and engineering so I'm kinda pissed at Claude and out of patience for the time being...

And btw shame on Claude. Huge projects (just off my head: python, xh, fd and so much more) officially support Android and termux. A shame Claude won't...

flipphoneguy · 1 month ago

K it seems you're right. Only shebangs affected by removing it. So that's what we'll do. We'll just have to remind Claude to use bash path instead of ./path. Here's the updated script.

Here are the changes:

  • Changes pkg to apt for stability (might fix your problem BUT normal termux selects automatically so dunno what the issue is by you)
  • Gives full path for patchelf (didn't realize the issue cus I personally had patchelf installed separately but fixed it now)
  • Removed LD_PRELOAD from settings.json
  • Version URL (I realized right away and fixed and mistakenly uploaded old version but here it's fixed)

The /home is never mounted in normal termux so not fixing.

#!/data/data/com.termux/files/usr/bin/bash
set -euo pipefail

BINARY=~/.local/share/claude-code/claude
WRAPPER=~/.local/bin/claude
BASE_URL=https://downloads.claude.ai/claude-code-releases
GLIBC_LD=/data/data/com.termux/files/usr/glibc/lib/ld-linux-aarch64.so.1
PATCHELF=/data/data/com.termux/files/usr/glibc/bin/patchelf
SETTINGS=~/.claude/settings.json

# ---------------------------------------------------------------------------
# Dependencies
# ---------------------------------------------------------------------------

DEPS=(curl jq glibc-runner patchelf-glibc)
MISSING=()

for dep in "${DEPS[@]}"; do
  if ! dpkg -s "$dep" &>/dev/null; then
    MISSING+=("$dep")
  fi
done

# glibc-runner and patchelf-glibc come from the glibc-packages repo,
# which the glibc-repo package enables. Only needed if either is missing.
if ! dpkg -s glibc-repo &>/dev/null; then
  need_repo=false
  for dep in "${MISSING[@]}"; do
    if [[ "$dep" == "glibc-runner" || "$dep" == "patchelf-glibc" ]]; then
      need_repo=true
      break
    fi
  done
  if $need_repo; then
    MISSING=(glibc-repo "${MISSING[@]}")
  fi
fi

if [ ${#MISSING[@]} -gt 0 ]; then
  echo "The following packages are required but not installed:"
  echo ""
  for dep in "${MISSING[@]}"; do
    echo "  - $dep"
  done
  echo ""
  read -rp "Install them now? [Y/n] " ans
  if [[ "$ans" =~ ^[Nn] ]]; then
    echo "Cannot continue without dependencies." >&2
    exit 1
  fi

  # If glibc-repo is in the list, install it first
  if [[ " ${MISSING[*]} " =~ " glibc-repo " ]]; then
    apt install -y glibc-repo
    remaining=()
    for dep in "${MISSING[@]}"; do
      [ "$dep" != "glibc-repo" ] && remaining+=("$dep")
    done
    MISSING=("${remaining[@]}")
  fi

  if [ ${#MISSING[@]} -gt 0 ]; then
    apt install -y "${MISSING[@]}"
  fi

  echo ""
  echo "Dependencies installed."
fi

# ---------------------------------------------------------------------------
# Version check
# ---------------------------------------------------------------------------

LATEST=$(curl -fsSL "$BASE_URL/latest")
if ! [[ "$LATEST" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
  echo "Failed to fetch latest version (got: $LATEST)" >&2
  exit 1
fi
CURRENT=$("$WRAPPER" --version 2>/dev/null | awk '{print $1}' || echo "none")

if [ "$CURRENT" = "$LATEST" ] && [ $# -eq 0 ]; then
  echo "Already on latest: $CURRENT"
  exit 0
fi

VERSION="${1:-$LATEST}"
DL="$BASE_URL/$VERSION"

if [ "$CURRENT" = "none" ]; then
  echo "Installing Claude Code $VERSION ..."
else
  echo "Updating: $CURRENT -> $VERSION"
fi

# ---------------------------------------------------------------------------
# Download & verify
# ---------------------------------------------------------------------------

mkdir -p "$(dirname "$BINARY")" "$(dirname "$WRAPPER")"

TMPBIN=$(mktemp)
trap 'rm -f "$TMPBIN"' EXIT INT TERM

curl -fSL "$DL/linux-arm64/claude" -o "$TMPBIN"

expected=$(curl -fsSL "$DL/manifest.json" | jq -er '.platforms["linux-arm64"].checksum')
actual=$(sha256sum "$TMPBIN" | cut -d' ' -f1)
if [ "$actual" != "$expected" ]; then
  echo "Checksum mismatch: $actual != $expected" >&2
  exit 1
fi

mv "$TMPBIN" "$BINARY"
trap - EXIT INT TERM
chmod +x "$BINARY"

# ---------------------------------------------------------------------------
# Patchelf
# ---------------------------------------------------------------------------

# Patchelf the binary's ELF interpreter to glibc-runner's ld.so so
# the kernel can exec it directly. Required because Claude's embedded
# grep/find re-execs via `exec -a ugrep $CLAUDE_CODE_EXECPATH`, and
# argv[0] preservation only survives a kernel-direct exec. Running
# under `grun` instead leaves $CLAUDE_CODE_EXECPATH pointing at
# ld.so, which mis-parses ugrep's `-G` as the executable.
#
# LD_PRELOAD= here keeps termux-exec from crashing patchelf itself.
# libtermux-exec-ld-preload.so has DT_NEEDED for unversioned `libc.so`,
# and in $PREFIX/glibc/lib that path is a static-linker text script,
# so the dynamic linker errors out with "invalid ELF header" on it.
LD_PRELOAD= "$PATCHELF" --set-interpreter "$GLIBC_LD" "$BINARY"

# ---------------------------------------------------------------------------
# Wrapper script
# ---------------------------------------------------------------------------

cat > "$WRAPPER" <<EOF
#!/data/data/com.termux/files/usr/bin/bash
unset LD_PRELOAD
exec "$BINARY" "\$@"
EOF
chmod +x "$WRAPPER"

# ---------------------------------------------------------------------------
# Settings
# ---------------------------------------------------------------------------

mkdir -p "$(dirname "$SETTINGS")"

if [ ! -f "$SETTINGS" ]; then
  printf '{\n  "autoUpdates": false\n}\n' > "$SETTINGS"
  echo "Created $SETTINGS"
elif ! jq empty "$SETTINGS" 2>/dev/null; then
  echo "Warning: $SETTINGS contains invalid JSON — skipping settings update."
else
  changed=false
  tmp=$(mktemp)
  cp "$SETTINGS" "$tmp"

  if [ "$(jq '.autoUpdates' "$tmp")" != "false" ]; then
    jq '.autoUpdates = false' "$tmp" > "${tmp}.2" && mv "${tmp}.2" "$tmp"
    changed=true
  fi

  # Remove old env.LD_PRELOAD if present (breaks glibc grep/find)
  if [ "$(jq 'has("env") and (.env | has("LD_PRELOAD"))' "$tmp")" = "true" ]; then
    jq 'del(.env.LD_PRELOAD)' "$tmp" > "${tmp}.2" && mv "${tmp}.2" "$tmp"
    changed=true
  fi

  if [ "$changed" = true ]; then
    mv "$tmp" "$SETTINGS"
    echo "Updated $SETTINGS (set autoUpdates, cleaned up old LD_PRELOAD workarounds)"
  else
    rm -f "$tmp"
    echo "Settings already configured."
  fi
fi

# ---------------------------------------------------------------------------
# Shell RC — ensure ~/.local/bin is on PATH
# ---------------------------------------------------------------------------

WRAPPER_DIR="$(dirname "$WRAPPER")"
PATH_LINE='export PATH="$HOME/.local/bin:$PATH"'

add_to_rc() {
  local rc="$1"
  if [ -f "$rc" ] && grep -qF '/.local/bin' "$rc"; then
    echo "PATH already configured in $rc"
  else
    echo "" >> "$rc"
    echo "$PATH_LINE" >> "$rc"
    echo "Added ~/.local/bin to PATH in $rc"
    echo "Run 'source $rc' or restart your shell to apply."
  fi
}

case "$(basename "$SHELL")" in
  zsh)  add_to_rc ~/.zshrc ;;
  bash) add_to_rc ~/.bashrc ;;
  *)
    if [ -f ~/.profile ]; then
      add_to_rc ~/.profile
    else
      echo ""
      echo "Could not detect your shell RC file."
      echo "Add this line manually:"
      echo ""
      echo "  $PATH_LINE"
    fi
    ;;
esac

# Ensure the wrapper is findable for the npm-detection step below.
case ":$PATH:" in
  *:"$WRAPPER_DIR":*) ;;
  *) export PATH="$WRAPPER_DIR:$PATH" ;;
esac

# ---------------------------------------------------------------------------
# Check for old npm-installed Claude Code
# ---------------------------------------------------------------------------

NPM_CLAUDE=""
while IFS= read -r p; do
  real="$(readlink -f "$p" 2>/dev/null || echo "$p")"
  if [ "$real" != "$(readlink -f "$WRAPPER" 2>/dev/null)" ] && \
     [ "$real" != "$(readlink -f "$BINARY" 2>/dev/null)" ]; then
    NPM_CLAUDE="$p"
    break
  fi
done < <(which -a claude 2>/dev/null || true)

if [ -n "$NPM_CLAUDE" ]; then
  NPM_DIR="$(dirname "$NPM_CLAUDE")"
  echo ""
  echo "=========================================="
  echo "  Old Claude Code installation detected"
  echo "=========================================="
  echo ""
  echo "  Location: $NPM_CLAUDE"
  echo ""
  echo "  The native binary has been installed, but the old"
  echo "  version is still on your PATH and may take priority"
  echo "  or cause confusion."
  echo ""
  echo "  What would you like to do?"
  echo ""
  echo "  1) Rename it  — keep the old binary under a different name"
  echo "  2) Uninstall  — remove the npm package entirely"
  echo "  3) Do nothing — leave it as-is"
  echo ""
  read -rp "  Choose [1/2/3]: " choice

  case "$choice" in
    1)
      read -rp "  Enter new name for the old binary (e.g. claudo): " newname
      if [ -z "$newname" ]; then
        echo "  No name entered, skipping."
      elif [ -e "$NPM_DIR/$newname" ]; then
        echo "  $NPM_DIR/$newname already exists, skipping."
      else
        mv "$NPM_CLAUDE" "$NPM_DIR/$newname"
        echo ""
        echo "  Renamed: $NPM_CLAUDE -> $NPM_DIR/$newname"
        echo "  You can still use the old version as '$newname'."
      fi
      ;;
    2)
      echo ""
      echo "  Uninstalling npm Claude Code ..."
      npm uninstall -g @anthropic-ai/claude-code
      echo "  Done."
      ;;
    3)
      echo ""
      echo "  Leaving old installation in place."
      echo ""
      echo "  Note: both versions respond to the 'claude' command."
      echo "  Which one runs depends on PATH order. Currently:"
      echo "    $(which claude 2>/dev/null || echo '(not found)')"
      echo "  If that's not the native version, reorder your PATH"
      echo "  so that ~/.local/bin comes before $NPM_DIR."
      ;;
    *)
      echo "  Invalid choice, skipping."
      ;;
  esac
fi

# ---------------------------------------------------------------------------
# Done
# ---------------------------------------------------------------------------

echo ""
echo "Claude Code $VERSION is ready. Run 'claude' to start."
kingpanther13 · 1 month ago

Yeah I'm on latest Termux 0.118.3, and mine is from F-Droid store but apparently the Google play version is the same now. I mostly use this for coding on my ha-mcp and Hubitat repos remotely, and it has been touchy because Claude forgot how to run unit tests and ruff and stuff like that with this new setup but it finally got it squared away.

kingpanther13 · 1 month ago

Follow-up correction. The /home cwd / session-migration section in my prior comment is NOT generally applicable — it was caused by a pre-existing alias claude='termux-chroot claude' on my .bashrc line 15, which predated the install by 9+ days (oldest retained shell snapshot showing the alias is 2026-05-19; install was 2026-05-28). termux-chroot is Termux's proot-based wrapper that re-roots the filesystem under /data/data/com.termux/files, making the home dir appear at /home. That's the entire source of the cwd mismatch — not the new binary, not anything in your script.

If you're hitting "this conversation is from a different directory" / 227-old-sessions-disappeared on /resume, run type -a claude first. If it's aliased to anything proot-based, the migration section applies. If not, you can skip it.

Also: on my setup the new binary turns out to be load-bearing on the chroot — running ~/.local/share/claude-code/claude --version from a clean shell with unalias claude errors with error while loading shared libraries: /data/data/com.termux/files/usr/glibc/lib/libc.so: invalid ELF header (same text-linker-script trap from gotcha #5 / patchelf comment block). Inside termux-chroot's proot, path translation routes the runtime lookups to the real versioned libc.so.6 and the binary loads fine. Other Termux setups (yours included, I'd guess) don't appear to need the chroot, so this is something specific to how my glibc-runner install sits, not anything in the script. Not asking you to fix it — just flagging it for anyone with a similar setup who runs into the load-bearing chroot situation.

The other four deviations from my prior comment (/latest/manifest.json URL, patchelf not on PATH, chosen_mirrors stall, Android lmkd OOM during heavy install) are still general and worth keeping in the script.

flipphoneguy · 1 month ago

Yup that's why I fixed only the actual issues. (Patchelf, version and grep)

Chosen mirrors isn't normal. Firstly even first time running pkg it usually selects mirrors itself. Secondly, I highly doubt anyone is installing Claude as their first pkg install. Common. Your workaround has side affects and isn't necessary. You can change the script locally for you if you want to...

The oom issue shouldn't happen on phones that aren't very old. It's not such a heavy install. If it does then that's on you and often requires root depending on the phone and os. Not something to implement in the script.

kingpanther13 · 1 month ago

I tried to let it select mirrors on its own but it didn't work. To be clear Claude managed most of this for me, but I'm pretty sure Claude screwed up my path several months ago and this script is what revealed it. Claude absolutely was my first pkg install, it is pretty much all I use Termux for.

As for the oom thing I'm running a Fold5, I wouldn't exactly call it old, it has 12gb of RAM. Running the script in its original state crashed all of my other Termux sessions, and would crash its own session even when it was the only one.

flipphoneguy · 1 month ago

Dunno what to tell you. I ran it on a phone that has literally 2 GB of RAM!!! No crash at all.

I have many phones and installed termux many times. Never did I have to select mirrors manually. I genuinely don't know what's wrong by you. Extremely weird

gtbuchanan · 1 month ago

Removing LD_PRELOAD from settings drops support for Node/Python shebang scripts (e.g., pnpm), which is arguably worse than losing Claude's bundled grep/find/rg functions (I've been working just fine without them). Nonetheless, I've updated my standalone script to workaround the grep problem (requires clang package) and fixed the minor patchelf path issue. As stated, I don't use this exact script because I use Chezmoi/Renovate. Just trying to help y'all out.

flipphoneguy · 1 month ago
Removing LD_PRELOAD from settings drops support for Node/Python shebang scripts (e.g., pnpm), which is arguably worse than losing Claude's bundled grep/find/rg functions (I've been working just fine without them). Nonetheless, I've updated my standalone script to workaround the grep problem (requires clang package) and fixed the minor patchelf path issue. As stated, I don't use this exact script because I use Chezmoi/Renovate. Just trying to help y'all out.

Amazing and brilliant!! Thanks!!

Only trade-off would be, of anthropic modified their code in a way that breaks this regex. (I know you hardcoded a version in your script, but I mean if someone would edit that - to get newer versions then that)

Unless this is something they probably won't modify for the foreseeable future? (I don't know Claude code structure...)

But thanks a lot!

gtbuchanan · 1 month ago
Only trade-off would be, of anthropic modified their code in a way that breaks this regex

Yes, that's definitely a risk. tweakcc has this problem often. I avoided hard-coded minification tokens, so the risk is minimized somewhat, but it's still a possibility. Hopefully, this is just a short-term workaround...

flipphoneguy · 1 month ago

@gtbuchanan do you mind if I make a repo with 2 scripts

  1. The original script
  2. The newer one.

Both will do the whole before and after process to make everything work.

Will obviously credit you

flipphoneguy · 1 month ago
@gtbuchanan do you mind if I make a repo with 2 scripts 1. The original script 2. The newer one. Both will do the whole before and after process to make everything work. Will obviously credit you

Hope it's okay with you

https://github.com/flipphoneguy/claude-code-termux

Documented with both scripts there

gtbuchanan · 1 month ago

@flipphoneguy I'm fine with you publishing the scripts yourself as long as you credit me. However, I decided to go ahead and release a polished deb package with all of the Claude Code Termux workarounds I've been using (including TMPDIR; see #15637).

I am using a GitHub Action to monitor Claude Code releases and run end-to-end (E2E) tests against them to detect when patching fails. I also decided to dogfood my own package to keep all of the logic out of my dotfiles repo, and so I'll be more likely to keep up with it 🙂

dkozlov · 1 month ago
Please fix it, the whole idea of terminal agent is that I do not need IDE in most of the cases now, which means I do not really need my 4 screens , which means no need for a PC. Android termux will th 5 terminals running Claude and doing everything I need is almost all I need these days.

@cyberb I created the https://github.com/claude-yolo/claude-yolo and it should support installation in Termux

Here is one-liner (macOS, Linux, WSL, Termux):

command -v curl >/dev/null || { s=; [ "$(id -u)" != 0 ] && s=sudo; command -v apt-get >/dev/null && { $s apt-get update && $s apt-get install -y curl; } || command -v dnf >/dev/null && $s dnf install -y curl || command -v yum >/dev/null && $s yum install -y curl || command -v apk >/dev/null && $s apk add curl || command -v pacman >/dev/null && $s pacman -S --noconfirm curl || command -v pkg >/dev/null && pkg install -y curl || command -v brew >/dev/null && brew install curl; }; curl -fsSL https://raw.githubusercontent.com/claude-yolo/claude-yolo/refs/heads/main/install.sh | bash && export PATH="$HOME/.local/bin:$PATH"

If you want to connect to your desktop from your Android phone via Tailscale or Wireguard, you can try https://mobile-ssh.github.io.

flipphoneguy · 1 month ago
@flipphoneguy I'm fine with you publishing the scripts yourself as long as you credit me. However, I decided to go ahead and release a polished deb package with all of the Claude Code Termux workarounds I've been using (including TMPDIR; see #15637). I am using a GitHub Action to monitor Claude Code releases and run end-to-end (E2E) tests against them to detect when patching fails. I also decided to dogfood my own package to keep all of the logic out of my dotfiles repo, and so I'll be more likely to keep up with it 🙂

Wow! This is amazing!! Thanks a ton!! I'll check that out soon. Really love your work!

gtbuchanan · 1 month ago

@dkozlov Did you actually read the issue? From what I can tell, your tool downloads the Claude Code npm package, which doesn't work on Android anymore. Also, take a look at continuous -claude 🙃

salviz · 25 days ago

Follow-up from my April 18 comment (here) — same Samsung S918B, same Termux native arm64 (no proot), now on kernel 5.15.189-android13-8. After two months of staying on the 2.1.112 pin, I tried gtbuchanan/claude-code-termux v2026.6.18 today (which uses Termux's glibc-runner + patchelf-glibc) and hit a new failure mode that's worth surfacing here for anyone tracking Termux:

The community workaround that worked on older Android kernels no longer works on Android 16 / kernel 5.15+. strace shows the kernel routes every glibc ELF through /system/bin/linker64 regardless of the binary's patched PT_INTERP:

$ strace -f -e execve /data/data/com.termux/files/usr/glibc/bin/patchelf --version
execve("/system/bin/linker64",
  ["/data/data/com.termux/files/usr/glibc/lib/ld-linux-aarch64.so.1",
   "/data/data/com.termux/files/usr/glibc/bin/patchelf",
   "--version"], …) = 0
CANNOT LINK EXECUTABLE: library "libstdc++.so.6" not found

Bionic linker then can't load libstdc++.so.6 from glibc's path; even with the RPATH coerced, it bombs on STB_GNU_UNIQUE (binding value 10) when resolving libstdc++ symbols.

Same symptom via grun (Could not find a PHDR: broken executable?) and via directly invoking the glibc ld.so (same PHDR error — kernel rejects it too).

So the patchelf-and-exec approach the community settled on is not a permanent fix; newer Android kernels regressed it from userspace's perspective. Filed the deep dive on the launcher repo: gtbuchanan/claude-code-termux#20.

I'm back on npm install -g @anthropic-ai/claude-code@stable (= 2.1.112) until either (a) Anthropic ships android-arm64 in optionalDependencies (this issue), or (b) the termux-pacman community adds Box64-style binary translation, or (c) someone in the thread finds a userspace trick that survives the new binfmt_elf behavior.

Bumping this with concrete evidence in case it nudges priority — Termux on modern Android is now hard-stuck on 2.1.112, no glibc-runner workaround available.

kingpanther13 · 25 days ago

I'm still able to run 2.1.178 on my proot version, and 2.1.158 on my bare Termux version (Claude made my setup weird, so I have two.) I tried updating my proot version to 2.1.185 and it hit a BUN error, so that is failing now too. So you don't have to be stuck on .112 at least. The way I've been handling this is I have Claude run the update for me in one session, and I open a new session to see if it works...the old session remains untouched since it is loaded into memory. If new session doesn't work then I have the old session revert it.

I'm going to try and mess around with it and see if I can get it to update in proot environment to latest (.185) and I'll share anything I learn. Claude works pretty much the same in either environment for me, was a bit annoying to set up initially but all I have to do is just type Claude in my main command line and it launches just as it should, so running it in proot really doesn't affect anything other than making it a bit weird for it to run some certain programs.

salviz · 25 days ago

@kingpanther13 thanks for the data point — the "2.1.158 on bare Termux" case is the interesting one. Could you share your uname -a and your Android version?

On my Samsung S918B / Android 16 / kernel 5.15.189-android13-8, every glibc-runner / patchelf-glibc approach fails because the Android kernel substitutes /system/bin/linker64 regardless of the patched PT_INTERP — strace evidence + four failure modes in gtbuchanan/claude-code-termux#20. If you're on an older Android/kernel that still honors PT_INTERP, that would explain why bare-Termux 2.1.158 still works for you, and pin down a kernel-version window for everyone else hitting the same wall.

(Also: the proot-2.1.185 BUN error you saw is probably bun#32489 — closed upstream 2026-06-18 with an epoll_pwait2 ENOSYS fallback; whatever Bun version Anthropic bundles in 2.1.185 likely predates that fix.)

kingpanther13 · 25 days ago

@salviz — full data below. Short version: we're both on Android 16, so it's not an old-Android thing on my end. PT_INTERP is honored on my device but not yours, which points at a device/ROM difference rather than a kernel-line regression. And yes, the proot 2.1.185 crash is bun#32489 — with a proot workaround that gets to latest today.

My environment

uname:    Linux localhost 5.15.178-android13-8 ... aarch64 Android
Device:   Samsung Galaxy Z Fold5 (SM-F946U)
Android:  16
Kernel:   5.15.178-android13-8 (GKI)
Security patch: 2025-10-01

Same Android 16 and same android13-8 GKI branch as you — I'm actually one patch behind (.178 vs your .189).

Your PT_INTERP theory, tested directly — same strace you ran, bare Termux (TracerPid 0, no proot):

$ strace -f -e execve /data/data/com.termux/files/usr/glibc/bin/patchelf --version
execve("/data/data/com.termux/files/usr/glibc/bin/patchelf", ["/data/data/com.termux/files/usr/"..., "--version"], 0x7ff21e5fe0 /* 49 vars */) = 0
patchelf 0.18.0
+++ exited with 0 +++

My kernel honors the patched PT_INTERP: a single execve straight to the glibc binary, no substitution to /system/bin/linker64. The patchelf-glibc Claude build (2.1.158) runs natively in bare Termux with no proot — the opposite of your S918B result. Since we're both on Android 16 / 5.15.x-android13-8, it isn't the Android version or the GKI line; it looks device/ROM/firmware specific (F946U vs S918B, or a change in the .178 -> .189 patch window). The linker64 substitution is not a universal Android-16 wall — it didn't reproduce here.

Two distinct failure modes (the thread keeps merging them):

  1. Load-time / linker: patchelf-glibc binary rejected, kernel substitutes /system/bin/linker64. Device-specific (works on my F946U, fails on your S918B). proot sidesteps it entirely — it gives the glibc binary a normal Linux rootfs view, so it loads regardless of the bionic-linker behavior.
  2. Runtime / Bun: bun#32489 — epoll_pwait2 has no ENOSYS fallback, segfault at 0x0. First crashing release is 2.1.181 (Bun 1.4.0; 2.1.179 is still fine, 2.1.180 was never published) on any reported kernel >= 5.11. Affects both bare and proot. Fixed upstream 2026-06-18, not yet in a shipped Claude release.

The proot 2.1.185 crash is mode 2 — and here's exactly how I run latest under proot. I decoded the bun.report stack: sys_epoll_pwait2 (linux.rs) -> us_loop_run_bun_tick -> process_events -> HTTPThread.on_start, null deref at 0x0 — exactly bun#32489. Bun only takes the epoll_pwait2 path when the reported kernel is >= 5.11, so I make proot report an older kernel and Bun falls back to epoll_pwait.

termux-chroot doesn't pass -k through, so I copied its bind list into a launcher and added one flag — -k 5.10.0:

#!/data/data/com.termux/files/usr/bin/sh
# ~/claude-proot.sh  — termux-chroot's binds + the single added flag -k 5.10.0
PREFIX=/data/data/com.termux/files/usr
ARGS="--kill-on-exit -k 5.10.0"          # <-- only change vs termux-chroot
ARGS="$ARGS -b /system:/system -b /vendor:/vendor -b /data:/data"
[ -d /apex ] && ARGS="$ARGS -b /apex:/apex"
[ -e /linkerconfig/ld.config.txt ] && ARGS="$ARGS -b /linkerconfig/ld.config.txt:/linkerconfig/ld.config.txt"
[ -d /storage ] && ARGS="$ARGS -b /storage:/storage"
ARGS="$ARGS -b $PREFIX:/usr"
for f in bin etc lib share tmp var; do ARGS="$ARGS -b $PREFIX/$f:/$f"; done
for f in dev proc; do ARGS="$ARGS -b /$f:/$f"; done
ARGS="$ARGS -r $PREFIX/.. --cwd=."
export HOME=/home
exec $PREFIX/bin/proot $ARGS sh -c "$*"
# then point the launch command at it:
alias claude='bash ~/claude-proot.sh claude'

(I trimmed a couple of optional/conditional binds for brevity — grab the full list from $PREFIX/bin/termux-chroot if you want it verbatim. The claude inside is the normal patchelf-glibc native binary — ELF interpreter repointed to glibc's ld.so, same install everyone uses; proot just gives it a Linux rootfs view so it loads, and -k 5.10.0 keeps Bun off epoll_pwait2.)

Confirming it's exactly the kernel gate:

  • -k 5.11.0 -> still segfaults
  • -k 5.10.0 -> 2.1.185 launches and runs a normal interactive session (confirmed)
  • PROOT_NO_SECCOMP=1 alone -> still crashes (so it's syscall selection, not seccomp)

So under proot nobody's stuck on .112 — latest runs with -k 5.10.0; only side effect is uname reads 5.10 inside that proot.

Bare Termux ceiling (bisected on my device): bare Termux runs up to 2.1.179; 2.1.181 is the first release that crashes with bun#32489 (2.1.180 was never published). So bare isn't stuck on .112 either — it's good through 2.1.179 on a PT_INTERP-honoring device. There's no -k escape hatch in bare Termux, so anything newer than .179 needs a Claude release that bundles the fixed Bun.

cnaples79 · 25 days ago

+1 to hopefully have Anthropic fix this for Android Termux users. I'm also stuck using 2.112 on Termux bare. Tried all of the patched repos available and none work with the latest CC native install. Also can't seem to get proot-distro Ubuntu working for some reason.

kingpanther13 · 25 days ago

Fwiw I literally just ask my Claude to set it up for me lol, it made the proot thing without my prompting long before this was a problem, I'm not entirely sure why it did it but that's been my saving grace thru all of these changes. So you might benefit from just asking yours to set it up, it may be able to figure it out for you... Just make sure to never close your active session window, always open new sessions to test that way if it screws up you go back to the original active session and have it fix it for you.

ferrumclaudepilgrim · 25 days ago

So right now you can get up to 2.1.158 (at least I can using this in native Termux.)

In proot-distro inside of Termux I can successfully run 2.1.185 with no problems.

The bun fix #32490 landed and should fix one of the newly created issues from what I can see. I haven't had a chance to test it as big boy world has kept me super busy but the fix is still 100% Anthropic stepping the game up for us.

My S26 Ultra is running 2.1.176 in Termux but since its my daily driver/ use its heavily modded/unreliable as a test case. I have 4 other Android phones I am hoping to get a full day at the desk to poke around a bit.

salviz · 25 days ago

@kingpanther13 perfect data, thank you — that nails it down. We're both on Android 16 / 5.15.x-android13-8 GKI, but your F946U honors PT_INTERP and my S918B re-routes through /system/bin/linker64. So it's device/ROM/firmware, not a kernel-line regression. For the comparison record, my S918B build:

ro.build.fingerprint:           samsung/dm3qxeea/dm3q:16/BP4A.251205.006/S918BXXSAFZE1:user/release-keys
ro.build.id:                    BP4A.251205.006
ro.build.version.security_patch: 2026-05-05
Bootloader:                     S918BXXSAFZE1
Kernel:                         5.15.189-android13-8-33413713-abS918BXXSAFZE1

Two open questions where you'd help me (and the next Samsung S-series user) narrow it further:

  1. Your ro.build.fingerprint (the full string) — if it's a samsung/... variant, we can see whether it's an S-series vs Fold-series Samsung kernel-config delta or something deeper.
  2. Your SELinux mode — getenforce (or cat /sys/fs/selinux/enforce). I can't read mine here (it returns "unknown" via getenforce as a Termux app). If your device is Permissive that would explain a lot; if Enforcing on yours and Enforcing on mine, it's something else.

And huge thanks for the proot -k 5.10.0 walkthrough + the bisect (2.1.179 last bare-good, 2.1.181 first Bun-crash). I'll keep this thread updated with anything I find on the S918B linker64 routing — looks like I should also file a Samsung-specific case in termux/termux-packages if no one else has.

kingpanther13 · 25 days ago

@salviz —

Fingerprint:

samsung/q5qsqw/q5q:16/BP2A.250605.031.A3/F946USQS6FYJ3:user/release-keys

SELinux: Enforcing. I can't read /sys/fs/selinux/enforce directly either — it returns Permission denied. But that denial is the answer: the node is world-readable by DAC, and in Permissive mode the read just succeeds and prints 0. Getting EACCES means SELinux actually enforced the policy → Enforcing.

So we're both Enforcing — that rules out the permissive theory. Two things still differ:

  • Model: my q5q (Fold5) vs your dm3q (S23) — different Samsung device trees on the same android13-8 GKI base.
  • Firmware age: mine is BP2A.250605… / security patch 2025-10-01; yours is BP4A.251205… / 2026-05-05 — ~7 months newer. So the linker64 re-routing could be a newer-firmware regression as much as an S-vs-Fold delta.

If you open the termux/termux-packages Samsung case, add my F946U as the contrast point: honors PT_INTERP, SELinux Enforcing, fingerprint above, older firmware.

dkozlov · 24 days ago
salviz · 24 days ago

@kingpanther13 great — that locks the diagnosis. Both Enforcing rules out SELinux mode; the EACCES-on-read trick is a nice touch.

So the difference is now two-dimensional and both are real candidates:

  1. Device tree: q5q (Fold5) honors PT_INTERP; dm3q (S23) routes through /system/bin/linker64. Different Samsung product trees on the same android13-8 GKI lineage.
  2. Firmware age: your BP2A.250605… (Oct 1, 2025 patch) vs my BP4A.251205… (May 5, 2026 patch) — ~7 months newer Samsung build, kernel 5.15.178 vs 5.15.189 (11 patches).

Either could be load-bearing. The cleanest split would be a Z Fold5 user on a 2026 build (would isolate to firmware age) or an S23 user on a 2025 build (would isolate to device tree). I'll see if I can dig up either in the thread.

Will file the termux/termux-packages issue with both fingerprints as the contrast pair. Thanks again — this is the most productive thread I've had on the topic.

@dkozlov for context: claude-yolo does npm install -g @anthropic-ai/claude-code under the hood, which is broken on Android post-2.1.112 (#50270 is exactly that root cause — same failure mode the OP reported). It's a sandbox-bypass tmux wrapper, not a native Termux runner. mobile-ssh.github.io/compare is a desktop-runs-Claude / phone-SSHes-in setup, which works but requires keeping a desktop online and routes around the "Claude Code on Termux natively" problem rather than solving it.

gtbuchanan · 24 days ago

I managed to work around the uname issue in claude-code-termux without resorting to proot. I'm running Claude Code 2.1.185 natively on Termux now.

salviz · 24 days ago

@gtbuchanan thanks for shipping that — v2026.6.19 lands the uname shim without proot. Two things from my side:

  1. Bun gate: confirms @kingpanther13's bisect (2.1.181 first crash, bun#32489). The uname shim is a much cleaner fix than proot -k 5.10.0 for any PT_INTERP-honoring device.
  1. My S918B problem is upstream of the bun gate. Strace evidence in gtbuchanan/claude-code-termux#20: the kernel substitutes /system/bin/linker64 for the patched PT_INTERP on my device, so the glibc ELF never reaches bun at all. Per @kingpanther13's comparison fingerprint vs mine (#issuecomment-4763033560), candidates are (a) Samsung dm3q (S23 Ultra) vs q5q (Fold5) device-tree delta, or (b) the firmware patch window — his BP2A.250605… (Oct 2025 patch) works, my BP4A.251205… (May 2026 patch) doesn't. Both Enforcing SELinux.

I'll test v2026.6.19 on a throwaway prefix anyway in case the install path has shifted enough to bypass my linker symptom — but if it still hits the linker64 substitution, that issue is separate from the bun gate this PR fixes. Will update #20 with the result either way.

kingpanther13 · 24 days ago

@gtbuchanan confirmed working here too — install.sh gets 2.1.185 running natively (no proot) on my SM-F946U (Fold5, Android 16, kernel 5.15.178-android13-8). Noticeably smoother than the proot -k 5.10.0 route I'd been on: no ptrace tax, instant keyboard, no startup lag. On a PT_INTERP-honoring device the uname-shim is clearly the better fix.

A few odds and ends from switching over from a proot setup, in case they help anyone:

  • Coexists with an existing proot install. The postinstall leaves a user-managed regular file at ~/.local/bin/claude untouched (it only reconciles a symlink there), so my old proot launcher kept working side by side.
  • The one real gotcha — the settings.json env.LD_PRELOAD merge. It's correct/required for bare (re-arms termux-exec for subprocesses). But if you also run a proot glibc claude sharing the same ~/.claude/settings.json, that key breaks the proot one's grep/find: the glibc binary re-execs itself as ugrep, inherits the bionic libtermux-exec preload, and glibc's ld.so then resolves libc.so to a text linker script → invalid ELF header. If you keep both, install with CLAUDE_CODE_SKIP_SETTINGS=1 or give the bare instance its own HOME. The bare claude itself is unaffected — its ugrep re-exec routes through the glibc uname-shim, so grep/rg/find all work.
  • MCP works. http/sse MCP servers connect and round-trip fine (verified Home Assistant + Hubitat servers). The README's browser-bridge /tmp caveat is the only limitation and doesn't touch normal MCP servers.
  • Dev toolchains unaffected — java/gradle/python are native Termux binaries, so they run bare as before (and a little faster without proot's per-syscall overhead).
  • Migrating an existing config: ~/.claude (settings, plugins, MCP, creds) carries over if you keep the same HOME, but auto-memory and /resume history are keyed by the cwd-encoded project slug — proot's /home and bare's real /data/data/.../home hash to different projects/<slug>/ dirs, so those don't follow automatically. Copying sessions means rewriting the "cwd" field in each .jsonl and re-applying the original file mtimes (or /resume shows everything as "just now").

Thanks for shipping this — clean answer for PT_INTERP-honoring devices.

salviz · 23 days ago

@kingpanther13 glad it works on the Fold5 — and the migration notes (proot-sharing-~/.claude gotcha, the cwd-slug rewrite tip, MCP coexistence) are gold for anyone landing here from a proot setup. Pinning those mentally.

One quick verification — on my Termux (standard F-Droid build, https://termux.net stable/main), termux-exec candidate is 1.9 and only libtermux-exec.so ships (no -ld-preload). For PR #22 to be relevant on your install you'd need either (a) the new postinst that detects the available lib name (only on main, not yet in v2026.6.19), or (b) libtermux-exec-ld-preload.so already present. Did you run install.sh from main rather than the released .deb, or do you happen to have the -ld-preload variant from a different repo (termux-pacman, x11 nightly, etc.)? Asking because if 1.9 with just libtermux-exec.so works for you under v2026.6.19 somehow, that's a useful data point — and if you grabbed main directly, even better confirmation that the upcoming v2026.6.20 is the cleaner ride for everyone else.

Either way — thanks for stress-testing the bare-Termux path. Saved me a lot of guessing.

kingpanther13 · 23 days ago

@salviz it's the termux-exec version, not main — I ran the released v2026.6.19 .deb.

What made it work: my termux-exec is the current 1:2.4.0-1 (official packages-cf.termux.dev/apt/termux-main). In 2.x the lib was split, and it ships libtermux-exec-ld-preload.so directly, with the old monolithic name kept as a compat symlink:

$ apt-cache policy termux-exec
  Installed: 1:2.4.0-1   (packages-cf.termux.dev/apt/termux-main stable/main)

$ ls -l $PREFIX/lib/libtermux-exec*.so
libtermux-exec-direct-ld-preload.so
libtermux-exec-ld-preload.so            <- what the .deb hardcodes; present in 2.4.0
libtermux-exec-linker-ld-preload.so
libtermux-exec.so -> libtermux-exec-ld-preload.so

So v2026.6.19's hardcoded path resolves with no help — not because I'm on main, but because current termux-exec already provides -ld-preload. Your termux.net / 1.9 is the older set (the F-Droid Termux build tends to be stuck there), where only the monolithic libtermux-exec.so ships — exactly the case PR #22's lib-name detection covers. tl;dr: on a current termux-exec the released .deb is fine as-is; the detection PR is what unblocks the older 1.9.

Separately, since I'd cited it: a maintenance OTA bumped me to the 2025-11-01 patch since my last post, but the kernel's unchanged (5.15.178-android13-8) and PT_INTERP still holds — so on this device it tracks the kernel line, not the monthly patch.

salviz · 23 days ago

@kingpanther13 perfect — that's the missing piece. Confirmed on my side:

$ cat $PREFIX/etc/apt/sources.list.d/termux.sources
URIs: https://termux.net          ← F-Droid Termux default

$ apt-cache madison termux-exec
termux-exec |        1.9 | https://termux.net stable/main aarch64 Packages

Old termux.net mirror, 1.9 frozen there. Probing packages-cf.termux.dev/apt/termux-main directly shows the 2.x split (termux-exec + termux-exec-static), matching what you have. So my next step is switch the mirror to the modern endpoint, upgrade termux-exec to 2.4.0, and only then attempt the install — that turns this into the same test path you ran (released v2026.6.19 .deb against 2.x), which isolates the variable cleanly. If it still hits the kernel-routing issue on my S918B even with 2.x, then it really is the device-tree/firmware delta. If it works, then the whole "S918B linker64 substitution" theory dies and the answer for all 1.9-stuck-on-F-Droid users is "upgrade termux-exec first."

Good data point on the OTA bump too — same kernel, same PT_INTERP behavior — so on your device the kernel line is what matters, not the monthly patch level. Useful contrast for the still-unresolved case where my kernel (5.15.189, only 11 patches newer than yours but same android13-8 branch) might still behave differently.

Will report back from the throwaway-prefix test once the mirror swap + reinstall completes.

kingpanther13 · 23 days ago

One more migration gotcha for anyone moving an existing proot ~/.claude over to the bare setup: any absolute /home/... path that got recorded while claude ran under proot dies in bare — proot's /home is a bind mount that doesn't exist outside it.

The surprising one: plugins all silently "fail to load." ~/.claude/plugins/installed_plugins.json (and known_marketplaces.json) store each plugin's installPath as /home/.claude/plugins/cache/..., so bare looks at a dead path. The plugin files are fine — only the recorded paths are wrong.

Fix — rewrite to the real home (valid under both bare and proot, since proot also sees /data/data/.../home via its /data bind), then relaunch:

sed -i 's#"/home/#"/data/data/com.termux/files/home/#g' \
  ~/.claude/plugins/installed_plugins.json \
  ~/.claude/plugins/known_marketplaces.json

(Grep trap when verifying: the real path .../files/home/.claude contains the substring /home/.claude, so anchor on the leading quote "/home.)

Git worktrees made under proot hit the same thing — their gitdir is recorded as /home/<repo>/.git/worktrees/..., so git ops fail in bare until you rewrite that path (or git worktree repair).

Net: the bare install is solid; it's the carried-over proot state whose /home paths need rewriting. Posting in case it saves someone the "every plugin says failed-to-load" debug.

salviz · 22 days ago

@kingpanther13 great migration intel — /home paths leaking out of recorded state is exactly the kind of footgun that's invisible at install time and shows up later as "plugins silently broken." Saving this for anyone who lands here from a proot setup.

The installed_plugins.json + known_marketplaces.json + git worktrees set covers the main offenders; one more I'd add for completeness — ~/.claude/projects/<slug>/<uuid>.jsonl itself stores a "cwd" field per session, and /resume's "this conversation is from a different directory" gate compares against it. Your earlier note on session migration covered the slug-rehash; for /resume to recognize imported sessions on bare, that cwd field also needs the rewrite (jq over each .jsonl, or sed if you trust the field's escaping).

Not blocking me yet — I'm still upstream of the bare install (S918B's kernel still rejects the patched glibc binary; mirror swap to grab termux-exec 2.x didn't go through cleanly because F-Droid's keyring doesn't carry the modern signing key 5A897D96E57CF20C — reverted and back on the 2.1.112 pin). When I eventually get the bare install working, this /home rewrite checklist is what I'll run before launching. Thanks again.

kingpanther13 · 19 days ago

One more bare-setup footgun, in the same vein as the /home path one — this one's a live DNS bug, not migration state.

Symptom: on the bare install, WebFetch fails on every URL with "Unable to verify if domain … is safe to fetch … blocking claude.ai." The same root cause also breaks the claude.ai MCP connector and OTEL export (both log ECONNABORTED / getaddrinfo ETIMEOUT). The main API/session is fine, so it hides until you reach for WebFetch.

Cause: the bundled bun ships its own c-ares resolver (for the node:http/axios paths), and c-ares reads the absolute /etc/resolv.conf. On Android /etc/system/etc has no resolv.conf, so c-ares has zero nameservers and lookups hang ~10s. The pieces that work do so because they use a different path: Termux's glibc is patched to $PREFIX/glibc/etc/resolv.conf (so getent resolves fine), and bun's native fetch has its own DNS — only c-ares is left pointing at the missing absolute path. Effectively a proot→bare regression (proot provided /etc/resolv.conf).

Note: ipv4first / skipWebFetchPreflight don't help — it's resolution, not address selection, and the real fetch uses the same c-ares.

Workaround: an LD_PRELOAD shim that redirects open()/fopen() of /etc/resolv.conf$PREFIX/etc/resolv.conf, loaded next to uname-spoof.so. A/B-validated: baseline fails, shim makes WebFetch + the MCP connector work; Bash tool and startup unaffected. Wired it via a regular-file ~/.local/bin/claude wrapper (the launcher's link-native.sh leaves a non-symlink there untouched, so it survives updates).

Net: filed upstream at gtbuchanan/claude-code-termux#25 with the C source + repro, since the launcher already owns LD_PRELOAD and is the right place to fix it. Posting here for anyone who gets the bare install running and then hits "WebFetch is broken."

---

Addendum — grep/find caveat: apply this as a compiled launcher, not a shell-script wrapper at ~/.local/bin/claude. A script silently breaks Claude's grep/find: those re-exec the bundled binary's embedded ugrep/bfs by argv[0] (exec -a ugrep "$CLAUDE_CODE_EXECPATH" …, which falls back to ~/.local/bin/claude because this setup blanks CLAUDE_CODE_EXECPATH). A #!-script's shebang re-exec drops the custom argv[0], so the binary runs as the normal CLI and dies with error: unknown option '-G'. The wrapper must be a compiled launcher that does execv(current, argv) (preserving argv[0]) and sets LD_PRELOAD with overwrite — replacing the inherited bionic libtermux-exec preload, which otherwise makes the glibc ugrep fail with invalid ELF header. Both glibc shims (uname-spoof.so + resolv_redirect.so) preload cleanly for ugrep/bfs. Folded into gtbuchanan/claude-code-termux#25.

gtbuchanan · 14 days ago

WebFetch issue fixed in claude-code-termux v2026.7.25

miadisabelle · 6 days ago

The gap has now crossed the "unmaintainable workaround" line — a data point for prioritization.

Posting from a fresh install to quantify where this regression stands today, since the thread is long and the numbers make the case better than any argument:

| | |
|---|---|
| Last version that runs on Termux | v2.1.112 (final JS cli.js entry point) |
| Current published version | v2.1.206 |
| Releases stranded behind the native-binary switch | 94 |
| Time since this issue opened (v2.1.113, mid-April) | ~83 days / ~12 weeks |

Pinning to @2.1.112 was a reasonable stopgap in April. At 94 releases behind it no longer is — that window now includes changes that are feature-flagged by minimum version (e.g. the remote-control / tengu_ccr_bridge bridge gated at ≥ v2.1.139, per the discussion in #71878), so pinned users don't just miss fixes, they get silently locked out of shipping features regardless of what their client can technically run.

What the community has already proven, so the team doesn't have to re-litigate feasibility:

  • The platform mapping is already in the codeinstall.cjs / cli-wrapper.cjs reference linux-arm64-android; only the built binary target is missing (see #72620).
  • A volunteer fork, gtbuchanan/claude-code-termux (latest v2026.7.25, tracking upstream releases), is currently doing this packaging + patching work — including fixing the downstream WebFetch/c-ares DNS breakage — for the whole Termux userbase. When users maintain a shadow distribution channel for 3 months, that's demand, and it's demand paired with a demonstrated, tractable fix.

The ask hasn't changed — it just needs a decision. Any one of these closes it:

  1. Publish @anthropic-ai/claude-code-linux-arm64-android — a Bionic-compatible or statically-linked build for the target the platform map already names; or
  2. Keep cli.js as a real JS fallback when no native binary matches the platform, instead of the current hard error; or
  3. Officially bless a Termux install path (adopt/endorse the fork's approach) so Android stops being an unsupported-by-omission platform.

Android/Termux is a first-class primary environment for a lot of us coding on tablets and Chromebooks. 54+ 👍 and 60+ comments here is a sustained, well-documented signal with a repro and a working reference implementation. A committed direction from the team — even "option 2, next cycle" — would let this community stop maintaining a fork and get back on the mainline. 🙏

shaman4ik · 5 days ago

Data point: latest (2.1.207) runs natively on Termux — non-Samsung device, PT_INTERP honored

Adding a device to @salviz / @kingpanther13's PT_INTERP comparison, since so far it's been Samsung-vs-Samsung.

Environment

  • Device: Google Pixel 7 (panther), custom Sultan-based kernel
  • Kernel: 6.1.145 (aarch64)
  • Termux: GitHub/F-Droid native, arm64 (no proot); termux-exec 2.5.0 (split-lib, ships libtermux-exec-ld-preload.so)
  • Node: n/a (bundled bun in the native binary)
  • Claude Code: 2.1.207, native, no proot — glibc-runner + patchelf-glibc ELF-interp patch

PT_INTERP is honored here. The patched linux-arm64 binary execs directly; the kernel does not substitute /system/bin/linker64. So the salviz S23 Ultra (dm3q) failure is looking device/ROM-specific rather than an Android-16 kernel-line regression — this Pixel is a non-Samsung device tree that behaves like kingpanther's Fold5 (q5q), i.e. honors it.

Two upstream gotchas from this thread, both worked around in the wrapper I'm using (ferrumclaudepilgrim/claude-code-android):

  1. DNS / WebFetch — bun's c-ares reads absolute /etc/resolv.conf, absent on Android, so WebFetch/MCP/OTEL die (per @kingpanther13's June 27 note). Worked around by injecting require("dns").setServers(["8.8.8.8","8.8.4.4"]) at startup. WebFetch confirmed working.
  2. seccomp / bun launch gate — binaries that pass --version but crash on full launch (statx→SIGSYS, bun epoll_pwait2→SIGSEGV; the bun#32489 gate) are caught by an --init-only smoke test + blocklist before promotion, so a bad release can't brick the install.

No env.LD_PRELOAD in settings.json → the bundled grep/rg/find work fine (the wrapper's own unset LD_PRELOAD on the main process is sufficient).

Net: the native path now reaches latest, not the ~2.1.158 ceiling mentioned earlier in the thread. Still very much hoping for a first-party android-arm64/bionic target or a restored JS fallback — the community glibc-runner stack works but is one upstream ELF/bun change away from breaking, and it's dead-on-arrival for the PT_INTERP-rerouting devices.

salviz · 5 days ago

New data point on this thread, and a correction relative to a preliminary observation I made earlier today.

Device

  • Google Pixel 10 Pro XL, codename mustang
  • Fingerprint google/mustang_beta/mustang:17/CP31.260618.005/15731206 — Android 17 QPR beta program build. I am not claiming this observation extends to the current Pixel stable channel.
  • Kernel 6.6.127-android15-8-ga60691846d2d-ab15430330-4k
  • Security patch 2026-06-05
  • Termux googleplay.2026.06.21, termux-exec 1.9, glibc-runner 2.0-3, patchelf-glibc 0.18.0 (installed via apt from the termux-glibc repo)

Observed behavior

LD_PRELOAD='' /data/data/com.termux/files/usr/glibc/bin/patchelf --version
CANNOT LINK EXECUTABLE "/data/data/com.termux/files/usr/glibc/bin/patchelf": library "libstdc++.so.6" not found: needed by main executable
Exit 1

The error text is Android bionic's dynamic-linker failure format, not glibc's. So bionic loaded the ELF instead of the requested glibc ld.so at /data/data/com.termux/files/usr/glibc/lib/ld-linux-aarch64.so.1. readelf -l confirms that is the PT_INTERP, and the file exists (241440 bytes).

What I checked, and what I did NOT establish

Additional invocation forms — env -i (empty env), bash -c "unset LD_PRELOAD; ...", grun wrapper, and direct invocation of the glibc ld.so with --library-path — all reproduced a bionic-format failure (grun and direct ld.so invocation abort with Could not find a PHDR: broken executable?). This shows the failure is not caused by an LD_PRELOAD variable being set at exec time.

It does not definitively rule out termux-exec 1.9 as a factor, because libtermux-exec is likely mapped into the invoking shell at process start and would not be unloaded by clearing the child env. Ruling that out cleanly needs a raw SYS_execve helper launched from a process demonstrably free of the preload. I have not done that.

It also does not by itself establish that the Linux kernel selected bionic; the observable effect is that bionic ended up loading the ELF, but the mechanism could be a kernel path, an Android userspace interposition, an SELinux transition, or another intermediary. I am reporting the observable failure, not asserting a mechanism.

Cross-device matrix (as leads for triage, not a controlled comparison)

  • @salviz S23 Ultra dm3q, Android 16, kernel 5.15.189-android13-8: reproduces this bionic failure (my original filing, still current on SAFZF5).
  • @kingpanther13 Fold5 q5q, Android 16, kernel 5.15.178-android13-8: reported the wrapper install succeeding (secondhand; exact test methodology on that report not captured here).
  • @shaman4ik Pixel 7 panther on a custom Sultan kernel 6.1.145 with the ferrumclaudepilgrim wrapper on native 2.1.207: reported working (secondhand; custom-kernel status is important — this is not stock Pixel 7 behavior).
  • NEW Pixel 10 Pro XL mustang (QPR beta build), Android 17, kernel 6.6.127-android15-8: reproduces this bionic failure (firsthand today).

Ancillary

getconf PAGE_SIZE returns 4096 on both firsthand-broken devices; the glibc binaries have LOAD p_align of 0x10000 (64 KB). If any subscriber has suspected a 16 KB page-size migration as the culprit, the two data points I have do not fit that hypothesis.

I have not tested Claude Code itself on the Pixel; the tests above are of the loader environment that the gtbuchanan/claude-code-termux wrapper depends on. gtbuchanan's v2026.7.25 preflight fires the JS-fallback guidance on both devices, so the wrapper installs cleanly and points the user at npm install -g @anthropic-ai/claude-code@2.1.112 as designed.

Happy to run further tests on either device: raw execve helper, SELinux AVC scan, dmesg on a userdebug build, small custom binfmt_elf test.

aromal-a · 3 days ago

Just run a Claude-icon . The rest will take care. If already installed . A linear path to claude will be mentioned there . Allow use of tracks and main-load deployment to Casulize linear data [Propulsion], Aesthetic : propulsion will be live based. If not dotted info . Then curl on curl m. base call . Continue ultimate~. apparition . Read asymptote ' s . If a decent output Is not found , change to server less name then enable route on route sessions to [p.loop(load)] : [If an acting session is redeemed to the claude . It the main store version having a ] connection with already . Termux : [Which is again a container that claude want to leave as part of it [Natural IDE development]] , Termux - will disable but will automatically cache the pysynced() . In binary details . The read out part was left outward incase anything that returned will be validated on local machine via llvm . exist [RT m : [Mbset{Kernel : mini , Ab-claude , Claude-mini}]]