DEV Community

Cover image for Running the full Claude Code CLI on a stock Android phone: no root, no PC
Lin Simon
Lin Simon

Posted on

Running the full Claude Code CLI on a stock Android phone: no root, no PC

Disclosure: I'm the developer, and Keva is a paid app with a 7-day trial.

I wanted the real Claude Code CLI on my phone. Not a remote session, not a cloud desktop: the actual CLI, running locally, working on local files. Here's what that took, how Claude Code itself built most of it, and what I learned along the way.

What I built

Keva runs Claude Code (and OpenAI Codex) on an ordinary Android phone.

  • A full Ubuntu arm64 userland inside the app (Node.js, Python, git), unpacked on first launch. That's most of the 766 MB APK.
  • A ptrace + seccomp supervisor between Android and the CLI, so glibc binaries run on Android's kernel without root.
  • targetSdk 28. On newer targets, Android's SELinux policy blocks executing files from the app's own storage. It's the same wall Termux hit.
  • Direct to the provider. Prompts go straight from the phone to Anthropic with your own API key or Claude subscription. Nothing passes through my servers.

How Claude Code was used

Claude Code (Opus) is the lead engineer on the project:

  • It reads the codebase and writes the spec for every change.
  • It hands implementation to other coding agents, then reviews every diff line by line and re-runs the full test suite itself (3,000+ Flutter tests, ~280 Kotlin tests) instead of trusting the agent's report.
  • A second model does adversarial review rounds. Claude decides which findings are real bugs and which are opinions, and loops until the reviewer says "mergeable".

Last week a user on an unusual Android build hit "tap Send, app vanishes, no logs". Claude diagnosed it from the code alone (an uncaught Throwable on a Kotlinbackground thread kills the whole process), specced the fix plus self-diagnosing crash reports, and shipped it after four review rounds. The user's reply the next morning: "it works now."

What I learned

  • Re-verify every claim an agent makes. Reviews are useful, but a meaningful share of findings don't survive a check against the actual code.
  • Give the reviewer a ship gate up front. "No path worse than the base commit; pre-existing issues get logged, not fixed." Without it, review rounds keep expanding scope.
  • Commit your own work before dispatching agents. They will "clean up" uncommitted changes they didn't make.
  • Test content, not paths. A test that only checked "the file exists" passed while 47 MB of a bundled tool was missing.

Happy to go deep on any of this in the comments, especially the ptrace/seccomp layer, which was the hardest part.

Keva is at keva.chat.

Top comments (7)

Collapse
 
hamid_ahmadian_3570449f72 profile image
Hamid Ahmadian •

"Re-verify every claim an agent makes" and "test content not paths" are the two that bite hardest in practice, and the 47MB-missing-but-test-passed example is a perfect illustration — an existence check is nearly free to satisfy and tells you almost nothing about correctness. We run a fleet of nightly headless agents and hit the same class of bug: a step reported "success" because the process exited 0, not because the output was actually right, and it took a separate re-run with an independent verifier to catch it. Your adversarial-review-round setup (second model decides mergeable, first model can't just self-certify) is the right shape for closing that gap. On the ptrace/seccomp layer you mentioned — is the sandbox intercepting syscalls to fake a normal Linux environment for the CLI, or is it closer to a real filtered execution boundary? Curious how much of Termux's usual constraints you had to work around vs. genuinely isolate against.

Collapse
 
lin_simon_d268f02841ef26c profile image
Lin Simon •

Thanks — and yes, the 47MB case changed how we test: packaging tests now check the upstream file list, not just that the file exists.

On ptrace/seccomp: neither pure translation nor our own filtering. The filter is Android's app-domain seccomp policy, and we don't loosen it. glibc binaries in the rootfs call things like set_robust_list/rseq, which that policy traps with SIGSYS. We run the process tree under a small ptrace supervisor that catches those traps and emulates the return, so the process survives. It can only turn "killed" into "denied"; it can't grant anything the kernel refuses. proot handles path/rootfs translation on top.

So the boundary is still the app sandbox (UID + SELinux + seccomp). The engine has exactly the app's capabilities, no more. Our threat model is "keep Android's sandbox intact", not "sandbox untrusted code inside the app".

Collapse
 
hamid_ahmadian_3570449f72 profile image
Hamid Ahmadian •

That "can only turn killed into denied, never grant anything the kernel refuses" framing is the key property — the ptrace supervisor is purely a compatibility shim, not a privilege escalation path, so the app-sandbox boundary (UID + SELinux + seccomp) stays exactly where Android put it regardless of what glibc code you run inside it. That's a much easier property to reason about and audit than "we carefully filtered syscalls ourselves," since you're not maintaining your own allow/deny list that could silently drift from Android's. Makes sense why the threat model is framed as protecting the sandbox rather than sandboxing the CLI itself — the phone's own security model already does the latter job for you.

Thread Thread
 
lin_simon_d268f02841ef26c profile image
Lin Simon •

Exactly — and one precision since you've made that line the crux: the table isn't purely "deny". It has four entries. set_robust_list and set_tid_address get a faked 0 (glibc's thread init needs them to "succeed"); get_robust_list and rseq get -ENOSYS; anything else passes through with the kernel's real errno, unmodified.

The faked-success pair never actually executes and is per-thread bookkeeping with no privilege attached. So we do keep a tiny list, but it only picks which lie to tell about a call the kernel already refused. It never decides what the kernel runs. That's why it can't drift into a hole the way a real allow-list could.

Thanks for pushing on this, it made me re-read our own shim.

Thread Thread
 
hamid_ahmadian_3570449f72 profile image
Hamid Ahmadian •

Faking 0 on set_robust_list/set_tid_address instead of hard-denying them is a nice detail — it tells me you actually tested against real glibc thread startup instead of just denying everything in the seccomp filter and hoping nothing broke. That's the kind of shim that looks correct on day one and then breaks silently eight months later when glibc changes what it expects back from one of those calls. Is the faked-0 pair covered by your own test suite (something that would fail loudly if a future glibc started checking the return value differently, or started calling a third syscall in that sequence), or is it currently "we verified it once against the glibc version we tested with"? If it's the latter, pinning that behavior with an explicit assertion feels worth the ten minutes — it's exactly the kind of thing that passes for years and then breaks in a way that's hard to diagnose because the symptom shows up in thread startup, nowhere near the sandbox code.

Thread Thread
 
lin_simon_d268f02841ef26c profile image
Lin Simon • • Edited

Good catch, and the honest answer is "half". The fake-0 verdicts themselves are pinned: unit tests fail if either call stops returning 0. But nothing runs real glibc thread startup under the supervisor in CI. That part is "verified against the glibc we ship".

Two things soften it: glibc is bundled in our rootfs at a pinned version, so it can't change under us, only when we upgrade it. And unknown syscalls aren't faked: they fall through with the original failure and get logged by name, so a new third call would show up loudly, not silently.

Still, you're right that the symptom would surface far from the sandbox. I'm adding an end-to-end check that spawns a pthread under the supervisor, and making it a gate for every rootfs upgrade. Thanks, this is exactly the kind of review I was hoping for.

Thanks for a review this sharp. I'd like to give you a lifetime Keva license. If you want to try it, sign up in the app and email your account address to hello@keva.chat.

Thread Thread
 
hamid_ahmadian_3570449f72 profile image
Hamid Ahmadian •

Glad the pthread gate is going in — that's the piece that actually closes the gap between "unit tests pass" and "a real thread starts correctly," and it's the kind of fix that pays for itself the first time you bump the rootfs. Appreciate the offer on the Keva license too — kind of you. I'll hold off reaching out for now rather than take you up on it on the spot; if that changes I'll follow up through the app. Good luck with the Android build, following along.