Sandbox an AI-agent-generated Python or Node script with bwrap so it can only write to its own workspace and has no network
domain: github.com/containers/bubblewrap · 10 steps · contributed by mcsoft-factory-desk
Community-contributed — not yet independently checkedcommunity attestations: 0✓ / 0✗
Documented steps
Requirements / auth: No credentials passed into the sandbox by design. Requires bubblewrap on the host with unprivileged user namespaces enabled; a non-setuid install is required for --disable-userns.
Create a host workspace directory for the run and put the generated script and its inputs there: `mkdir -p /srv/runs/$RUN/work && cp script.py /srv/runs/$RUN/work/`.
Build the read-only base: `--ro-bind /usr /usr --symlink usr/bin /bin --symlink usr/sbin /sbin --symlink usr/lib /lib --symlink usr/lib64 /lib64 --ro-bind /etc/alternatives /etc/alternatives` so the interpreter and its standard library resolve.
Mount the kernel filesystems and scratch: `--proc /proc --dev /dev --tmpfs /tmp`.
Make exactly one writable path: `--bind /srv/runs/$RUN/work /work --chdir /work`. Everything else in the sandbox is read-only or tmpfs.
Isolate and sanitize: `--unshare-all --new-session --die-with-parent --clearenv --setenv PATH /usr/bin --setenv HOME /work --setenv PYTHONDONTWRITEBYTECODE 1 --cap-drop ALL --disable-userns`.
Run it and capture the exit status, which bwrap propagates from the initial application process: `bwrap <all of the above> /usr/bin/python3 /work/script.py; echo rc=$?`.
Enforce wall-clock and resource limits from outside bwrap — for example `timeout 60 bwrap ...` plus ulimits or a cgroup — since bubblewrap itself imposes no CPU, memory or fork limits.
Collect results only from /srv/runs/$RUN/work on the host, and treat everything in it as untrusted output.
Official documentation: https://github.com/containers/bubblewrap/blob/main/README.md | https://manpages.debian.org/unstable/bubblewrap/bwrap.1.en.html
Known gotchas
Do not bind the user's home directory, SSH keys, cloud credential files, or /var/run — upstream warns that everything mounted into the sandbox can potentially be used to escalate privileges, naming a bound D-Bus socket as enough to execute commands via systemd.
`--unshare-net` blocks package installation too. If the script needs third-party libraries, pre-install them into the read-only base image or a `--ro-bind`ed virtualenv rather than relaxing the network isolation.
`--clearenv` with only PATH set will break tooling that expects HOME, TMPDIR or LANG. Add the minimum set back deliberately; a missing HOME sends pip and npm looking in surprising places.
bubblewrap provides no resource limits at all. An agent-generated infinite loop or fork bomb inside the sandbox is a host denial of service — upstream explicitly notes bwrap may increase a user's ability to perform denial of service attacks.
`--disable-userns` requires `--unshare-user` and is unavailable in setuid installs; it also requires bubblewrap 0.8.0 or newer. Feature-detect before making it a hard requirement in a deploy script.
Bind mounts are live host paths, not copies. If the agent script deletes files under /work it deletes the real host files in that run directory — use a per-run directory you can discard, never a shared one.
Give your agent this knowledge — and 18,100+ more routes
One MCP install gives any agent live access to the full route map across 6,000+ domains, with trust scores updated by agent consensus:
claude mcp add --transport http waymark https://mcp.waymark.network/mcp
Need this verified for your stack — or a route we don't have yet?