4. Isolate with bwrap: drop all capabilities, scrub the environment, invert /root to default-deny#
Date: 2026-05-11
Status#
Accepted
Context#
We need process isolation that a rootless-podman devcontainer can run
unprivileged, with no CAP_SYS_ADMIN and a strict seccomp profile. The embedded
predecessor used unshare -m plus tmpfs overlays (see
3. Live in a standalone repo, extracted from python-copier-template); we wanted something stronger and declarative — an
argv you can read top-to-bottom rather than a sequence of imperative mount
steps.
Decision#
Build the isolation as a single bwrap argv: --cap-drop ALL, --clearenv
plus an explicit environment allow-list, --unshare-{pid,ipc,uts},
NO_NEW_PRIVS, and — the load-bearing move — invert /root to default-deny:
--tmpfs /root, then re-bind back only an allow-list of paths (.claude,
.claude.json, .cache, .config/{gh,glab-cli}, .local/share). Home is
default-deny; you opt paths in, you never blacklist them out. Credential
isolation is therefore decided in this allow-list (bwrap_argv_build), not in
any advisory check (see 14. Keep the integrity-check surfaces separate and self-contained).
Consequences#
A new credentialed tool that drops files under
$HOMEor$HOME/.config/<tool>/is masked for free — the forward-compatible default is “hidden.”Every primitive is provable:
/verify-sandbox’s 18-check battery maps each row ofREADME-CLAUDE.md’s defence table to the bwrap primitive that enforces it.The argv builder is kept inline and pure so
tests/bwrap_argv.shcan assert over the built argv directly. Which categories under$HOMEflip polarity is refined in 11. Split the home re-binds by XDG category; where the config that drives these binds is read from is fixed by 12. Treat the read-write workspace as untrusted: default to $PWD, source config from /etc.