Context
Once proot-distro is installed (hack 1) and the Ubuntu rootfs is in place, actually launching proot requires a long command with many flags. Bind mounts expose host filesystems into the guest. Environment variables configure the guest shell. Special flags handle Android filesystem limitations.
Typing or remembering this command is impractical, especially over SSH on a phone keyboard. The helper script captures the exact invocation that works and makes it a single command. Other scripts (start-openclaw, restart-gw) call run-proot.sh rather than duplicating the proot flags.
This hack is marked LEGACY. The native migration on Feb 15, 2026 eliminated proot entirely. The gateway now runs directly on Termux with node22-icu and an LD_PRELOAD compat shim, removing the 6-layer stack in favor of 4 layers.
Implementation
#!/data/data/com.termux/files/usr/bin/bash
# run-proot.sh — Launch proot Ubuntu environment for OpenClaw
# Deploy to: $PREFIX/bin/run-proot.sh
set -euo pipefail
PREFIX="/data/data/com.termux/files/usr"
ROOTFS="$PREFIX/var/lib/proot-distro/installed-rootfs/ubuntu"
PROOT="$PREFIX/bin/proot"
# Load environment variables from .openclaw/env if it exists
ENV_FILE="$ROOTFS/root/.openclaw/env"
ENV_ARGS=()
if [ -f "$ENV_FILE" ]; then
while IFS='=' read -r key value; do
# Skip comments and blank lines
[[ "$key" =~ ^#.*$ || -z "$key" ]] && continue
ENV_ARGS+=("-e" "$key=$value")
done < "$ENV_FILE"
fi
exec "$PROOT" \
--link2symlink \
--kill-on-exit \
--root-id \
--rootfs="$ROOTFS" \
--bind=/dev \
--bind=/proc \
--bind=/sys \
--bind="$PREFIX:/termux-prefix" \
--bind=/data/data/com.termux/files/home:/termux-home \
--cwd=/root \
"${ENV_ARGS[@]}" \
/usr/bin/env -i \
HOME=/root \
LANG=C.UTF-8 \
TERM="$TERM" \
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
/bin/bash --login "$@"Deploy the script:
cat > $PREFIX/bin/run-proot.sh << 'SCRIPT'
#!/data/data/com.termux/files/usr/bin/bash
# ... (paste full script above)
SCRIPT
chmod +x $PREFIX/bin/run-proot.shUsage from other scripts or directly:
# Interactive login shell
run-proot.sh
# Run a single command inside proot
run-proot.sh -c "node --version"
# Used by start-openclaw to launch the gateway
run-proot.sh -c "cd /root && node /root/.openclaw/gateway/dist/index.js"Key flags explained:
--link2symlink: Translates hard link syscalls to symlink. Required because Android's filesystem does not reliably support hard links from proot userspace. Without it, npm install fails when hardlinking files in node_modules.--kill-on-exit: When the proot parent dies (OOM kill, SSH disconnect), all child processes inside the guest are also killed. Prevents orphaned Node.js processes from consuming RAM silently.--root-id: Makes the process appear as UID 0 inside the guest. Without it, apt and npm complain about permission errors writing to/usr/and/root/.--bind=/dev,--bind=/proc,--bind=/sys: Expose kernel interfaces. Node.js needs/proc/self/for process introspection, and some npm packages read/sys/for hardware info.--bind=$PREFIX:/termux-prefix: Makes Termux binaries (git, curl, etc.) accessible from inside proot at a predictable path.
Verification
run-proot.shdrops into a bash shell withwhoamireturningroot.ls /termux-prefix/bin/shows Termux binaries accessible from inside proot.ls /termux-home/shows the Termux home directory.echo $HOMEreturns/root,echo $LANGreturnsC.UTF-8.echo $PATHshows only Linux paths, no Termux paths leaked.run-proot.sh -c "cat /root/.openclaw/env"shows the loaded API keys (confirms env loading works).run-proot.sh -c "ls /proc/self/status"succeeds (confirms /proc is bound).
Gotchas
- The
env -iinvocation strips any leaked Termux environment variables. Without it,$PATHinside proot might include Termux paths like$PREFIX/bin, causing the wrong binaries to be found (e.g., Termux's old Node v12 instead of proot's Node v22). - The env file at
$ROOTFS/root/.openclaw/envmust useKEY=VALUEformat with no spaces around=. Quoted values likeKEY="value with spaces"are passed literally including the quotes. Do not useexportprefixes in the file. - The
--bind=/devmount is effectively read-only from proot's perspective. Writing to/dev/inside the guest does not affect the host device nodes. - On the Moto E2 with 1 GB RAM, proot itself adds roughly 10-15 MB of RSS overhead per process due to syscall interception. Every filesystem operation goes through proot's ptrace-based translation. This overhead was one of the key motivations for the native migration.
- If the
ENV_ARGSarray is empty (no env file), the"${ENV_ARGS[@]}"expansion produces nothing, which is correct. But ifset -ucatches an unbound variable, add a fallback:"${ENV_ARGS[@]+"${ENV_ARGS[@]}"}". - The
--cwd=/rootflag sets the initial working directory. If/rootdoes not exist in the rootfs (corrupted install), proot silently falls back to/, which can break scripts that assume they start in$HOME.
Result
A single run-proot.sh command launches a correctly configured proot Ubuntu environment with all bind mounts, environment variables, and compatibility flags. Other scripts call it as a building block, avoiding duplicated proot flags across the codebase. The script was the backbone of the proot-based stack until the native migration made it obsolete, reducing the stack from 6 layers to 4 and eliminating the per-syscall proot overhead.