johnstowers.co.nz ~ / blog / 2026/04/15 / sandbubble

sandbubble - a python sandbox for untrusted code

sandbubble runs LLM- and agent-generated python inside a lightweight sandbox. It isolates code you did not write and have not reviewed, so it cannot touch your home directory or your ssh keys, without the overhead of docker or a VM.

It is a single file, sandbox.py, that you drop into your project. There is no build step and nothing to containerise, no daemon, and essentially no startup cost. The unit of isolation is a python virtualenv; you can keep a venv with your private dependencies and have that venv be the execution sandbox.

Under the hood it sets up a fresh bubblewrap container: a tmpfs root, read-only binds for the system directories (/usr, /lib, /bin, /sbin), isolated /proc, /dev and /tmp, and access to the venv tied to the python executable. Network is on by default (--disable-network turns it off), and one writable host directory is bind-mounted at /mnt for getting data in and out. AppArmor, via aa-exec, constrains both the bwrap launcher and the payload.

Setup is a one-off. You run sandbox.py as a script and it works out the exact AppArmor profile your virtualenv needs, writes it out, and prints the cp and apparmor_parser commands to install it; the profile is plain text, so you can read it before you load it as root. With the profile installed, running the script again executes a suite of self-checks that confirm the isolation properties actually hold (an optional sandbox_extra_tests.py adds an extended --extra-tests suite). After that you use sandbox.py as a module: get_default_sandbox_config(), a SandboxConfig, and run_sandboxed() or run_sandboxed_async() to run code. It needs a Linux box with AppArmor enabled, bubblewrap, the aa-exec userspace tools, and a virtualenv.

sandbubble is a defensible isolation layer, not a hardened sandbox. It is suitable for an internal application stack, but it is not perfect and a determined attacker may be able to escape it. The kernel stays in the trusted computing base, so it gives no protection against kernel 0-days or a novel container escape. There is no denial-of-service resistance by default; for cpu, memory, disk or output limits you add cgroups and rlimits yourself. It is not formally verified, and small changes to the mounts or the profile move the attack surface around. Network is on by default, so exfiltration is possible; that is a convenience choice, not a safety one.

The scope is deliberately narrow. It is pragmatic isolation for running your own internal LLM or agent code with a reasonable, inspectable boundary; it is not for hostile public input or multi-tenant workloads, and for those you want something stronger. The code is at github.com/nzjrs/sandbubble .