Claude Code runs on an Android phone, inside Termux, with nothing else. No PC over adb, no root, and no proot Ubuntu. Three facts make this possible:
- Since Bun 1.4.2 there is an official Android build of Bun.
- The
claudebinary is an executable compiled with Bun, and the JS source inside it can be pulled back out. - Run that JS with the Android Bun and you are done.
This post is the step-by-step guide, followed by a short explanation of why it works.
How is this different from proot + Ubuntu?
Until now, almost every way of running Claude Code on a phone came down to booting a whole Ubuntu under proot and running it in there. Termux's proot-distro does this, and apps like Operit bundle PRoot and an Ubuntu 24.04 rootfs inside the APK to do the same thing. This guide skips Ubuntu and runs directly on Android.
It is not all upside. The proot route gives you a real Ubuntu, so nearly any Linux program you can apt-get will run. Here, only things that have a Termux package or are pure JS will run, and you have to re-extract every time Claude Code updates. Still, if all you want is Claude Code, this is far lighter and faster than lifting an entire Ubuntu for it.
0. What you need
- An Android phone (arm64, which means any recent phone). I tested on Android 16
- Termux, preferably the F-Droid or GitHub release build
- A Claude subscription (Pro/Max)
- About 1 GB of free storage
1. Base packages
pkg update -y
pkg install -y curl unzip file ripgrep
2. Install Bun for Android
This is the step that matters most.
Do not use the official bun.sh install script (
curl bun.sh/install | bash). It only looks atuname, downloads the regular Linux (glibc) Bun, and you getunexpected e_type: 2. You have to download the -android build yourself.
cd ~
curl -fLO https://github.com/oven-sh/bun/releases/latest/download/bun-linux-aarch64-android.zip
unzip -o bun-linux-aarch64-android.zip
mkdir -p ~/.bun/bin
mv bun-linux-aarch64-android/bun ~/.bun/bin/bun && chmod +x ~/.bun/bin/bun
rm -rf bun-linux-aarch64-android*
echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc
file ~/.bun/bin/bun # should say "pie executable ... interpreter /system/bin/linker64"
bun --version
3. Save the extraction script
This is the ~50-line script that pulls the JS out of the claude binary. Paste the whole block into Termux and it is saved as ~/unbun-claude.ts. It is short enough to read before you run it, and I recommend that you do.
cat > ~/unbun-claude.ts <<'EOF'
// Extract the JS module graph from a `bun build --compile` executable (here: the
// `claude` binary) into a plain directory that a regular `bun` can run.
//
// Layout (Bun 1.4.x StandaloneModuleGraph): [graph bytes][Offsets: u64 byteCount,
// u32 modulesOff, u32 modulesLen, u32 entryId, u32 argvOff, u32 argvLen, u32 flags]
// ["\n---- Bun! ----\n"]. Each module record is 13 u32s. The format is Bun-internal,
// so a record-size mismatch is refused rather than guessed at.
import { mkdirSync, writeFileSync, readFileSync } from 'fs';
import { dirname, join, resolve } from 'path';
const [bin, outArg] = process.argv.slice(2);
if (!bin || !outArg) throw new Error('usage: bun unbun-claude.ts <claude-binary> <outDir>');
const out = resolve(outArg);
const buf = readFileSync(bin);
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
const trailer = buf.lastIndexOf(Buffer.from('\n---- Bun! ----\n'));
if (trailer < 0) throw new Error('no Bun module graph trailer found');
const o = trailer - 32;
const byteCount = Number(view.getBigUint64(o, true));
const modulesOff = view.getUint32(o + 8, true);
const modulesLen = view.getUint32(o + 12, true);
const entryId = view.getUint32(o + 16, true);
const base = o - byteCount;
const RECORD = 52;
if (modulesLen % RECORD) {
throw new Error(
`module table length ${modulesLen} is not a multiple of ${RECORD}: graph format changed`,
);
}
const slice = (off: number, len: number) => buf.subarray(base + off, base + off + len);
const PREFIX = '/$bunfs/root/';
const TEXT = /\.(m?js|cjs)$|^cli$/;
let entry = '';
const count = modulesLen / RECORD;
for (let i = 0; i < count; i++) {
const r = base + modulesOff + i * RECORD;
const u = (k: number) => view.getUint32(r + k * 4, true);
const name = slice(u(0), u(1)).toString('utf8');
const rel = name.startsWith(PREFIX) ? name.slice(PREFIX.length) : name.replace(/^\/+/, '');
let body: Buffer | string = slice(u(2), u(3));
// Chunks import each other by absolute /$bunfs/root/ path; point them at the extraction dir.
if (TEXT.test(rel)) body = body.toString('utf8').replaceAll(PREFIX, out + '/');
const dest = join(out, rel === 'cli' ? 'cli.js' : rel);
mkdirSync(dirname(dest), { recursive: true });
writeFileSync(dest, body);
if (i === entryId) entry = dest;
}
// Some code resolves its own entry as "<root>/cli"; keep both names.
writeFileSync(join(out, 'cli'), readFileSync(entry));
console.log(`extracted ${count} modules → ${out}\nentry: ${entry}`);
EOF
4. Download Claude Code and unpack it to JS
The claude binary ships inside the claude-agent-sdk-linux-arm64 package on npm. Download it and let the script pull out the JS.
cd ~
# look up the latest version, then download
V=$(bun -e "fetch('https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/latest').then(r=>r.json()).then(j=>console.log(j.version))")
echo "SDK $V"
curl -fsSL "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-$V.tgz" | tar xz
# extract (unpacks into ~/claude-js)
bun unbun-claude.ts package/claude ~/claude-js
rm -rf package
If you see something like extracted 2122 modules → ..., it worked.
5. Create the claude command
cat > $PREFIX/bin/claude <<EOF
#!$PREFIX/bin/sh
export USE_BUILTIN_RIPGREP=0
exec $HOME/.bun/bin/bun $HOME/claude-js/cli.js "\$@"
EOF
chmod +x $PREFIX/bin/claude
claude --version # "2.1.xxx (Claude Code)" means OK
USE_BUILTIN_RIPGREP=0 tells Claude Code to use the ripgrep you installed in Termux instead of the Linux rg bundled with it. Without it the file search tools break.
6. Log in
claude auth login
Open the printed URL in your phone's browser, approve, copy the code, and paste it back into Termux.
It is
claude auth login, notclaude login.claude loginis not a subcommand, so it opens an interactive session with "login" as the prompt, and it just looks like it hung.
7. Test
claude -p "reply with exactly: pong"
If pong comes back, you are done. From now on just type claude in a project folder.
Common errors
Why this works (for the curious)
- Claude Code's
claudeis a single executable built withbun build --compile. It is only distributed for glibc/musl Linux, and the Android linker refuses both. - But the tail of that executable holds Bun's module graph, and each chunk in it is stored as the original JS source, next to its bytecode.
- So you parse the module table, write each module out as a file, and rewrite the
/$bunfs/root/...paths that chunks use to import each other so they point at the extraction folder. After that a regular Bun can run it. - Bun 1.4.2 added a build for Android (Bionic libc), and that is what makes it possible to run this JS directly on the phone.