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.

SELinux denies link() in app-private storage, and distro rootfs tarballs are full of hardlinks (Debian's /usr/bin/perl5.40.1perl). 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.