Covers: Why run the code at all, and what can go wrong; The layers, and what each one is for and more.
Why run the code at all, and what can go wrong
Models are bad at exact arithmetic and good at writing programs. A code tool turns a guess into a computation: the model writes the program, the sandbox runs it, the output comes back. That makes an agent a data analyst, a coding assistant and a researcher who can check its own sums.
There are three ways this goes wrong, and only one of them needs an attacker.
A mistake. The program loops forever, fills the disk, allocates all the memory or prints fifty megabytes. No malice; the model simply wrote while True or [0] * 10**9.
An instruction hidden in data. The agent reads a web page, an email or a file, and the text says "run this code". This is prompt injection (module 1, and again in module 7). The model is not the attacker; the content is, and the model is its hands.
A trusted helper that is tricked. The code the model writes is sandboxed, but the host side of the sandbox (the code that copies files in and out) runs with the host's privileges and takes names chosen by the sandboxed program. If it is careless, the sandboxed program uses it to read what it could not read directly (section 8).
Sandbox, blast radius, defence in depth
A sandbox is an environment in which untrusted code can run and the damage it can do is bounded by something other than the code's own good behaviour. The blast radius is what is harmed if the worst happens. Defence in depth means several independent layers, so that one failing does not decide the outcome. The notes show one case where layering is also a trap: one layer can hide another layer's defect (section 11).
The layers, and what each one is for
The layers around the program. The outer ones are about time and resources; the inner ones are about what the program can see and reach.
Each layer is a field in a Policy, and six presets turn them on one at a time so the experiments can show what each buys.
Level
Adds
Real mechanism
L0 bare
nothing (a 4-second watchdog so the experiment cannot hang)
a child process
L1 limits
wall-clock limit, CPU, memory, file size, open files, output cap, sweep of leftovers
setrlimit, a polling loop, a process session
L2 env and cwd
scrubbed environment, private working directory
a fresh environment and a temporary directory
L3 processes
own process tree, a cap on the number of processes
a PID namespace and a monitor that walks /proc
L4 network
no way out to the network
a network namespace
L5 filesystem and capabilities
chosen paths hidden, no capabilities
a mount namespace, setpriv
Limits and supervision
Time. The run is stopped after a wall-clock limit (1.5 seconds in the examples) and the result says timed_out, because "it finished" and "it was stopped" must look different to the reader. The kill reaches the whole tree: the process, its children, and everything in its session.
Processor time, memory, file size, open files. These are kernel resource limits set in the child before it starts. A program that allocates past 256 MB gets a MemoryError in Python (the limit is on address space, and the result sets an oom flag from the error text, which is a heuristic). A program that writes past 10 MB gets an error from the write. A program that opens more than 64 files gets "Too many open files".
A limit that does nothing for the user who matters. The obvious limit on the number of processes (RLIMIT_NPROC) is not enforced for root, and a sandbox often runs as root. I tried it, even inside a user namespace: a program asked to start 30 processes started all 30 with the limit set to 5. So the example enforces the cap with a monitor: every 5 milliseconds it walks /proc, counts the processes under the run, and kills the run when the count passes the limit (16). A real deployment would use a control group; the lesson is to test that a limit bites, not to assume it.
A quirk I did not expect. A busy loop under a 2-second CPU limit stopped after 2.0 seconds with no namespace, and after 3.0 seconds inside a process-id namespace. The reason: the first process in a PID namespace ignores signals whose default action is to terminate it, so the soft limit's signal does nothing, and only the hard limit (one second later, which cannot be ignored) stops it. The sandbox sets the hard limit one second above the soft one. If you set only the soft limit, your CPU limit would not work in that configuration.
Output. The harness never holds more than 8 MB of any program's output, and the policy returns at most 8,000 characters of each stream: the first 60 percent and the last 40 percent, with a marker saying how many bytes were left out. The end matters because the end of a Python error is the line that names the problem. A defect that keeps only the beginning is one of the planted ones.
Leftovers. When a run ends normally, a child it started may still be running. The example sweeps the run's session after every run. That is not enough, because a program can start a new session (a detached "daemon" process) and escape the sweep, which is why the next layer exists.
Namespaces: seeing less
A namespace gives a process its own private view of one kind of system resource. They are the foundation of containers, and the sandbox uses four of them through the unshare command.
Process-id namespace. The program sees only its own processes, and when the first one ends, the kernel kills all the rest. A program that starts a detached background process and exits cannot leave it running. In the experiment the detached "orphan" survived at L0, L1 and L2 and was gone at L3.
Network namespace. The program gets a network with no interfaces up, not even this machine's own loopback address. A connection attempt fails at once with error 101 ("Network is unreachable"). The honeypot web server on the host's loopback is not reachable from inside, which is how the experiment proves that a program could not send anything out.
Mount namespace. Paths can be replaced for the program only: a directory by an empty one, a file by /dev/null. The real files are untouched on the host. The experiment hides a "home directory" that holds a fake SSH key. Writing into the hidden directory also goes nowhere.
User namespace. Needed to do the other three without being the real root. The program is "root" inside it. The last step, setpriv --no-new-privs --bounding-set=-all, removes every capability and prevents gaining any, so that "root" cannot even mount a filesystem (a test tries and fails).
Fail closed
If the machine cannot create a namespace (a kernel with user namespaces turned off, a locked-down container, a different operating system), the example refuses to run and raises SandboxUnavailable. The alternative, quietly running without the isolation the policy asked for, is a defect I planted (fail_open), and a check catches it. A sandbox that stops sandboxing without saying so is worse than none, because everyone believes it is there.
The environment and the working directory
Two cheap layers close two real holes.
A scrubbed environment. A child process inherits the parent's environment variables, and that is where API keys live. At L2 the program starts with only PATH, LANG, HOME and TMPDIR (both pointing into its own directory). Claude Code's own documentation makes the same point: its sandboxed commands inherit the parent's environment by default, credentials included, and it offers a setting to strip them.
A private working directory. At L0 and L1 the program runs in the application's own directory, and one of the attacks simply overwrites a file there. From L2 each sandbox session gets a fresh directory with owner-only permissions. It lives across several runs (so a program can leave a file for the next one) and is deleted when the session ends.
Neither layer helps against a program that reads a file by its full path. That takes the filesystem layer.