Handbooks  /  Sandboxing
Handbook~15 min readSecurityworked math + runnable code
The Sandboxing Handbook

Run code you
don't trust.

Agents write code now — to analyze data, use a tool, or script their way to a goal — and then something has to run it. That code is untrusted by definition: it might be buggy, or manipulated by an injected instruction, or simply wrong in a way that wipes a directory. Sandboxing is how you run it anyway, safely. The trick isn't listing everything dangerous and forbidding it — you'll always miss one. It's the opposite: forbid everything, then allow back the tiny set the task actually needs. This handbook is that inversion, made concrete.

01

Code you can't trust

The moment an AI system executes code it generated — a Python snippet to crunch a spreadsheet, a shell command to inspect a repo, a tool the model wrote itself — you've introduced untrusted code into your process. And it's genuinely untrusted: the model can be wrong, and worse, a prompt injection in some document it read could steer it into writing code that exfiltrates data or deletes files. Running that directly on your host is a loaded gun pointed at your own foot.

Sandboxing lets you run it anyway by isolating it: the code executes in a contained environment where it can only touch what you explicitly permit and only consume the resources you allot. If it misbehaves, the damage is confined to the sandbox — the blast radius stays small. The whole discipline is about defining that containment correctly, and the single most important decision is the default: what happens to an operation you didn't think about?

The one-sentence version

Run untrusted code in isolation where everything is denied by default and only a minimal allowlist of operations is permitted, bounded by resource caps — so even malicious code can't escape.

02

Deny by default

There are two ways to write a security policy. A block-list tries to enumerate everything dangerous and forbid it — no rm -rf, no network to these hosts, no reading that file. It feels natural and it's almost always wrong, because you cannot list every bad thing. Attackers (and confused models) find the one path you forgot, and there's always one you forgot.

Deny-by-default inverts the logic: everything is forbidden unless it's on an explicit allowlist of permitted operations. Now you only have to enumerate the small, well-understood set of things the code legitimately needs — read this file, call this API — and anything not on that list is denied automatically, including the attacks you never imagined. You can't enumerate all evil, so you enumerate the little good instead. This is the foundational rule of every serious sandbox: the default answer to "may I?" is no.

03

Least privilege

Deny-by-default asks you to write the allowlist — and how you size it is the principle of least privilege: grant only the minimal permissions the task needs, nothing more. A script that summarizes one file needs to read that one file — not write anywhere, not open the network, not spawn processes. Every permission you add back is a door you've opened.

Smaller allowlist, bigger denied surface
All opsread · write · network · spawn · env · … — the universe of things code could do.
Allow: {read}One door open; everything else denied — the escape surface the sandbox blocks.
Allow: {read, net}Two doors — more useful, but a larger attack surface.
RuleGrant the fewest ops the task needs — minimize the doors.

There's a precise relationship here: the smaller the allowlist, the larger the set of denied operations — the escape surface the sandbox successfully blocks. Every operation you don't allow is one the code can't use to escape or cause harm. So least privilege isn't just tidy; it's directly proportional to safety. Grant read-only when read-only will do, and the code physically cannot delete, exfiltrate, or persist — no matter what it tries.

04

The policy math

A sandbox is a decision function over operations. Deny-by-default means an operation is permitted only if it's on the allowlist A; the denied set — the escape surface — is everything else:

allow(op)  =  [ op ∈ A ]     escape surface = Ops ∖ A  (grows as A shrinks)

Least privilege = keep A small, so Ops ∖ A (what the sandbox blocks) is large. Every op not in A is one the code can't perform.

But policy is only half. Even an allowed operation can run away — an infinite loop, an allocation bomb — so you add resource caps: an operation runs only if it's both permitted and within budget:

run(op)  =  ( op ∈ A )  AND  ( cost ≤ cap )   ⟹   else denied

Policy says what is allowed; caps say how much. You need both — same bounding idea as an agent harness's step/budget limits. The runnable version below is a deny-by-default policy with a resource cap and the escape surface.

RUN IT YOURSELF

A deny-by-default policy

A sandbox is a policy plus a budget. Deny-by-default: an operation runs only if it's on the allowlist — anything else is denied automatically, so the escape surface is every op you did not permit. Least privilege shrinks that allowlist, which grows the denied surface the sandbox blocks. And a resource cap rejects even an allowed op if it would exceed the budget, so permitted code still can't run away. A policy check runs before the op can execute. Change the allowlist or budget and watch what gets denied.

CPython · WebAssembly
05

Layers of isolation

Real sandboxes enforce the policy at several levels — the more independent layers, the harder to escape (the same defense-in-depth logic):

LayerWhat it isolates
Process / OSRestrict syscalls, filesystem, and network at the kernel level (seccomp, namespaces, cgroups).
ContainerA locked-down container with no host mounts, no ambient credentials, read-only where possible.
microVM / VMHardware-level isolation for the strongest boundary (e.g. Firecracker) — untrusted code gets its own kernel.
Resource capsCPU time, memory, wall-clock timeout, disk, and output size — bound even permitted work.
No secrets, no egressDon't mount credentials or allow outbound network unless the task truly needs it — least privilege for data.

Two defaults do most of the work: no network egress (so code can't exfiltrate or phone home) and no credentials mounted (so even if it reads the filesystem, there's nothing to steal). Give the code an ephemeral, throwaway environment with only the specific inputs it needs, and destroy it after. This pairs directly with the harness: the harness bounds how many steps and how much money; the sandbox bounds what each step can touch. Together they contain an agent that can run code.

06

Pitfalls

The most common mistake is a leaky sandbox that feels safe: running "isolated" code that still has network access, a mounted secret, or a writable host path. Isolation is only as strong as its weakest allowed permission — one forgotten door defeats the whole wall. Audit what's actually reachable from inside, not what you intended. A close cousin is reusing environments: if one task's sandbox persists into the next, malicious state (a planted file, a poisoned cache) carries over — make sandboxes ephemeral and per-task.

Two more. Forgetting resource caps: an allowlist stops forbidden operations but not an infinite loop or an allocation bomb using permitted ones — always cap CPU, memory, and time. And trusting the sandbox's outputs: code may be contained, but whatever it returns flows back into your system (and maybe the model's context), so treat sandbox output as untrusted too and validate it at the boundary. Sandboxing is an exercise in humility: assume the code inside is hostile, grant it the least you can, bound how much it can do, and give it nothing worth stealing. Do that and you can safely let an agent run the code it writes.

Worth knowing

A sandbox is only as strong as its most permissive allowed permission. "Isolated" code with network egress or a mounted secret isn't isolated — one forgotten door defeats the wall. Default to no network, no credentials, ephemeral per-task environments, and audit what's actually reachable from inside.

Frequently asked

Quick answers

What is sandboxing?

Running untrusted (e.g. agent-generated) code in an isolated environment where it can only do what you explicitly permit, containing the blast radius.

Why deny-by-default?

You can't list every dangerous operation, so forbid everything and allow back only the minimal set the task needs — the secure default.

What is least privilege?

Grant the fewest permissions the task requires; a smaller allowlist means a larger denied escape surface and less possible harm.

Why resource caps?

An allowlist controls which operations, not how much — caps on CPU, memory, and time stop even permitted code from running away.

Finished this one? 0 / 99 Handbooks done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.