←Back

2026-09-19

Running Claude Code Natively on Android (Termux Only, No proot)

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:

  1. Since Bun 1.4.2 there is an official Android build of Bun.
  2. The claude binary is an executable compiled with Bun, and the JS source inside it can be pulled back out.
  3. 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.

proot + UbuntuThis guide (native)
Speedproot intercepts every system call with ptrace to rewrite paths. The more files a task touches (npm install, git, file search), the slower it getsNo interception layer. Same speed as any other Termux program
DiskA few hundred MB of Ubuntu rootfs, plus Node.js and friends from aptOne Bun binary and a folder of extracted JS
StackAndroid → Termux → proot → Ubuntu → Node → Claude Code. Many layers, so it is hard to tell which one brokeAndroid → Termux → Bun → Claude Code
Phone file accessUbuntu is an isolated file system, so phone storage and Termux files are only visible through bind mountsIt is the Termux environment, so Claude can directly use whatever you installed with pkg, and Termux:API commands like termux-open-url
Battery / heatEvery system call costs an extra process switch, so it burns more CPUNo such overhead

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 at uname, downloads the regular Linux (glibc) Bun, and you get unexpected 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, not claude login. claude login is 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

SymptomCause / fix
unexpected e_type: 2You installed the glibc Bun (the bun.sh install script). Replace it with the -android build as in step 2. The other possibility is that you ran the original claude binary instead of the extracted cli.js
claude login hangsIt has to be claude auth login
Remote Control is rejectedA setup-token token created on a PC (CLAUDE_CODE_OAUTH_TOKEN) is inference-only, so Remote Control will not work with it. Run unset CLAUDE_CODE_OAUTH_TOKEN (and remove it from .bashrc if you put it there), then log in again with claude auth login
clipboard / audio warningsThose native addons are glibc builds and fail to load. They are optional features, so you can ignore the warnings
Updating Claude CodeThere is no auto-update. Redo step 4 to get the new version. If Bun's internal format changes some day, the extraction script stops with an error instead of guessing

Why this works (for the curious)

  • Claude Code's claude is a single executable built with bun 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.