Claude Code 2.1.113 dropped cli.js: pinning 2.1.112 on Android
Why does claude stop working on Android once @anthropic-ai/claude-code moves past 2.1.112? Starting with 2.1.113, the package's bin.claude entry points to bin/claude.exe instead of cli.js, so Shelly pinned 2.1.112, the last release that ships cli.js as a pure-JS entry point.
The sections below cover three questions: why the native build fails on Android bionic, what the pin runs and where it stops, and which credential files a pinned Shelly install needs before claude starts.
Why does 2.1.113 fail on Android bionic?
The native-loading problem involved the @anthropic-ai/claude-code-linux-arm64-musl binary on Android bionic. In claude-code@2.x, the top-level bin/claude.exe is "only a 500 B stub", as an on-device Termux trace showed on 2026-04-18.
The real ET_EXEC sits in that optional sub-package and declares /lib/ld-musl-aarch64.so.1 as its interpreter. musl libc is an implementation of the C standard library [2]. Alpine Linux is "a security-oriented, lightweight Linux distribution based on musl libc and busybox" [1].
Three layers stood between Android and that binary: package installation, the loader and execution policy.
At the packaging stage, npm on Android refuses the musl sub-package with EBADPLATFORM. Loading fails next, because bionic's loader rejects the musl linker's ELF program header layout. At execution, SELinux blocks direct execve on app_data_file.
What does the 2.1.112 pin run, and where does it stop?
Commit 008b43f29 (2026-04-19) pinned Claude Code to 2.1.112 and also carried a cp -al staging fix. An empirical comparison of the @anthropic-ai/claude-code tarball across versions selected that release.
Its tarball contains LICENSE.md, README.md, cli.js and vendor/.
On the Termux side, the pin is a single install command:
npm i -g @anthropic-ai/claude-code@2.1.112
To prevent auto-update, DISABLE_AUTOUPDATER=1 and chmod a-w are listed as measures to consider. These packaging facts come from tarball inspection and an on-device trace, not from an Anthropic page.
| Aspect | Before (v29-v30 native dispatch of 2.x) | After (v33 pin) |
|---|---|---|
| Dispatch | v29: bundled Alpine musl linker; v30: existing proot wrapper | cli.js (2.8 MB JS) |
| Runtime | v29 failed with "Could not find a PHDR: broken executable?" when /system/bin/linker64 attempted to load Alpine's ld-musl-aarch64.so.1 |
bundled bionic node, unmodified |
| Version label | v29, v30 | BASHRC_VERSION 33 |
What does the pinned build need to log in?
A pinned build alone did not get past bug #102 (claude OAuth 400); the practical workaround was a credentials transplant from a Termux install where claude already works. That workaround was verified on 2026-04-20, 09:30 to 10:04 JST, with Termux, wireless ADB and a Shelly device.
Claude Code's authentication state is split between ~/.claude.json and ~/.claude/.credentials.json. The first, 32 KB and directly under $HOME, holds the onboarding-completion flag and the master copy of the account information. OAuth access and refresh tokens live in the second, a 470 B file. Without both, the onboarding screen appears.
Replacing /tmp/claude, adding an xdg-open wrapper and transplanting ~/.claude/ without ~/.claude.json did not complete authentication in the verified session. The sed rewrite of the hard-coded /tmp/claude to $HOME/.claude-tmp left no matches, yet claude still returned 400. An am start wrapper at $HOME/bin/xdg-open resolved through which xdg-open, while claude kept entering manual paste mode. A 948 MB tar of the whole ~/.claude/ directory still ended at onboarding. Adding ~/.claude.json was the last piece, bringing up "Welcome back" and one successful round-trip in Japanese.
Export from Termux:
cp ~/.claude.json /sdcard/Download/shelly-claude-root.json
tar czf /sdcard/Download/termux-claude-dir.tar.gz -C ~/.claude .
gunzip -k /sdcard/Download/termux-claude-dir.tar.gz
Import into Shelly:
cp /sdcard/Download/shelly-claude-root.json ~/.claude.json
chmod 600 ~/.claude.json
cd ~/.claude
tar xf /sdcard/Download/termux-claude-dir.tar
claude
Shelly's tar is hard-wired to /bin/zcat, which is why the export also produces an uncompressed copy. The Shelly import runs chmod 600 ~/.claude.json after copying the file.
On success, the startup screen shows Claude Code v2.1.112 with the "Welcome back" greeting. A first launch may ask "Quick safety check: Is this a project you created or one you trust?". Choosing 1. Yes, I trust this folder stops that prompt from reappearing.
What the copies carry and replace
Every exported file lands in /sdcard/Download: the account master as shelly-claude-root.json, plus the ~/.claude archive in both .tar.gz and .tar form. Because that archive is taken from the whole ~/.claude directory, the OAuth tokens in .credentials.json travel inside it.
Size is dominated by history.jsonl and file-history, which make up most of the 1.8 GB termux-claude-dir.tar. Those three files therefore leave tokens, account data and session history outside both apps' home directories until they are removed.
On the receiving side, extraction overwrites Shelly's entire ~/.claude/, and Termux-era session history comes along with it. Anything already stored in Shelly's ~/.claude/ is replaced by the Termux copy at that step.
After the access token expires
The expiresAt field in .credentials.json is the access token's expiry, about 9 hours in the verified session. After expiry, claude-code refreshes internally with the refresh token, and that refresh has been reported as blocked by Cloudflare WAF (#47754).
Recovery from a blocked refresh means completing /login again on Termux and re-transplanting both files. An auto-update of the Termux source to 2.1.113+ removes cli.js and makes /login itself impossible (#50270).
Other approaches
Dispatch through the bundled musl linker (v29, commit 1a59dd898). This attempt targeted the loading barrier by reusing ld-musl-aarch64.so.1, which Shelly already bundled for codex through the Alpine minirootfs in $HOME/.shelly-rootfs/. It also reworked __shelly_bg_cli_update to force-install the musl sub-package. Bionic's own loader remained the constraint, since linker64 would not load the musl linker on the device.
Dispatch through proot (v30, commit 9734ecbff). The replacement used the existing proot wrapper, which chroots into the bundled Alpine minirootfs and uses its own ptrace-based loader. Inside that chroot, /lib/ld-musl-aarch64.so.1 resolves naturally from PT_INTERP, and a $HOME:/root bind exposes the binary at /root/.shelly-cli/node_modules/@anthropic-ai/claude-code-linux-arm64-musl/claude. A kernel-level chroot, by contrast, requires elevated privileges for operation [3].
The follow-up v31, which addressed "all 5-agent review blockers for v2.x support", was reverted on 2026-04-18 with 3751 deletions.
What to check
- Which entry point is installed? Read
bin.claudein the global package'spackage.json; "cli.js" matches 2.1.112, while "bin/claude.exe" marks 2.1.113. - Is
bin/claude.exeroughly 500 bytes? A file that small is the 2.x stub, not the executable. - Does a forced install still lack the sub-package? npm needs
--libc=musl --forceand fetches@anthropic-ai/claude-code-linux-arm64-muslonly when it is named explicitly. - Does
linker64print the PHDR error from the table? That error is the loader barrier, so the musl-linker route will not get further on bionic. - Onboarding or the login-method screen after a transplant? Check that
~/.claude.jsonwas copied too, since.credentials.jsonalone leads to the login-method screen. - Does
tar xzffail with "/bin/zcat not found"? Use the uncompressed.tarwithtar xf. - Does Shelly's
~/.claude/hold anything worth keeping? Back it up beforetar xf, because extraction overwrites the whole directory. - Are the transplant files still in
/sdcard/Downloadafterclaudestarts? The archive there carries.credentials.jsonwith the OAuth tokens, andshelly-claude-root.jsonis the account master. - Did
claudework, then stop hours later? Compare the clock withexpiresAtin.credentials.json. - Has Termux's
claude-codechanged version? Run the 2.1.112 pin there before the next/login.
Limit: The transplant was verified in a single session with one Shelly device, and the refresh failure after expiresAt rests on a report in #47754 rather than that session.
Sources
- [1] altinn.studio
- [2] libc.org
- [3] man7.org
The author's own notes
AI disclosure: this post was researched, written and reviewed by AI tools. Publishing: a post that passes automated checks may be published automatically, before the author reads it.
Comments
No comments yet. Start the discussion.