Field notes
Seven things Android actually does to a Linux engine — every one hit for real on a stock, unrooted Android 16 phone while bringing engine v0 up. Each cost a debugging round; recorded so they cost nobody a second one.
1 · Static Go binaries can't resolve DNS
Android has no /etc/resolv.conf. A CGO-free Go binary uses the pure-Go
resolver, which — finding no config — falls back to localhost:53 and dies
with connection refused. Bionic-linked binaries get DNS from Android's
resolver daemon; static binaries get nothing.
Fix: when the file is absent, install a custom
net.DefaultResolver that dials public DNS directly, rotating servers per
attempt (a UDP “dial” succeeds even to an unreachable server, so in-dial fallback is
an illusion). CELLAR_DNS overrides.
2 · …or verify TLS certificates
Same story for CA roots: nothing at /etc/ssl. Termux ships a bundle at
$PREFIX/etc/tls/cert.pem; Android keeps the system store in
/apex/com.android.conscrypt/cacerts (14+) or
/system/etc/security/cacerts.
Fix: probe those locations and set
SSL_CERT_FILE/SSL_CERT_DIR before the first handshake —
Go reads them exactly once.
3 · Hard links are forbidden in app data
SELinux denies link() in app-private storage, and distro rootfs
tarballs are full of hardlinks (Debian's /usr/bin/perl5.40.1 →
perl). Plain tar -x fails with
Cannot hard link … Permission denied.
Fix: run the extraction itself under
proot --link2symlink -0, which rewrites link() into
symlinks — proot-distro's own battle-tested recipe.
4 · suid file modes break your own tooling
Alpine ships /bin/bbsuid as mode 4711 — setuid,
executable, not readable. You own the extracted file but can't read it:
export breaks, and read-only directories break recursive delete. suid grants nothing
under proot anyway.
Fix: normalize owner bits after extraction
(u+rw files, u+rwx dirs), and again before deletion for
partially extracted trees.
5 · proot's seccomp fast path breaks npm
npm install inside a machine dies with npm's infamous
Exit handler never called! and an otherwise-empty debug log — while
node itself runs fine. The culprit: proot's seccomp acceleration interacting with
node/npm's process management.
Fix: default PROOT_NO_SECCOMP=1 for guests —
correctness over speed (CELLAR_SECCOMP=1 re-enables the fast path for
benchmarking). With it set, Claude Code installs and runs.
6 · npm's rename dance fails under proot
Reinstalling a global npm package renames the existing dir aside; under proot the
rename dies with ENOTEMPTY. First install works, second breaks.
Fix: catalog stacks are idempotent — detect and skip instead of reinstalling over a live tree.
7 · os.Exit skips your deferred cleanup
Not Android's fault, but found here: an error path that exits the process means
deferred cleanup never runs — leaving invisible half-created machines
on disk.
Fix: explicit cleanup on error paths, a SIGINT/SIGTERM handler
during create, and — because SIGKILL can't be caught — interrupted machines surface
as broken in cellar ls, removable with
rm --force.
Proof it all works
$ cellar create dev --distro debian # 90 MB, sha256-verified, ~1 min $ cellar apply dev claude-code $ cellar exec dev -- claude --version 2.1.220 (Claude Code)
An AI agent harness, running inside a rootless Linux machine, on a phone. The debugging chain above is why the quickstart is six commands instead of a wiki.