Code execution is the most useful tool you can give an agent and the most dangerous. Data analysis, transformation, chart generation, test running, scripted remediation — all of it needs a real interpreter. And the moment you provide one, anything that can influence the agent's context can influence what code runs on your infrastructure.

The threat model is not "the model writes buggy code." It is "an attacker writes the code and the model submits it." Everything about sandbox design should follow from that assumption.

The Escape Paths That Actually Get Used

In practice, incidents cluster far away from exotic kernel exploits.

Credential harvesting from the environment. The sandbox process inherits environment variables, has a mounted Kubernetes service account token at a well-known path, or can reach cloud instance metadata at 169.254.169.254. Three lines of code and the agent's runtime credentials are in the output. This is the single most common real-world sandbox incident, and it requires no vulnerability at all — only a permissive environment. See SSRF risks in agent tool calls for the metadata half.

Network egress. The code opens an outbound connection and posts whatever it has. Data from the analysis task, secrets found in the environment, internal service responses. No escape needed; the sandbox is doing exactly what it was built to do, over a network path nobody restricted.

Reachability into internal services. The sandbox sits inside the VPC. Internal APIs that authenticate by network position are directly callable.

Shared writable state. A cache directory, a package directory, or a scratch volume shared across executions or tenants. Code from one execution plants something that a later execution picks up — persistence across a boundary that was assumed to be per-task. Package directories are the classic vector: write a malicious module under a name a later task will import.

Resource exhaustion. Fork bombs, memory exhaustion, tight loops, filling the disk. Not an escape, but a denial of service against everything sharing the host, and trivially triggered.

Actual container escape. Privileged containers, mounted Docker sockets, excess capabilities (CAP_SYS_ADMIN), host mounts, or an unpatched kernel vulnerability. Rare relative to the above, catastrophic when it happens, and almost always a misconfiguration rather than a novel exploit.

Containers Are Not Sufficient On Their Own

A standard container shares the host kernel. Namespaces and cgroups isolate the view and the resources; they do not remove the kernel's syscall surface. A kernel vulnerability reachable from inside the container is an escape.

That does not make containers useless — it means a container is the starting configuration, hardened deliberately:

  • Drop all capabilities, add none back unless a specific requirement is documented. --cap-drop ALL.
  • Read-only root filesystem, with one small tmpfs scratch mount, noexec where feasible.
  • Non-root user, with no-new-privileges set so setuid binaries cannot elevate.
  • Seccomp profile restricting syscalls to what the runtime needs. This is the control that most directly shrinks kernel attack surface, and the default Docker profile is a floor, not a target.
  • No Docker socket, no host mounts, no host network, never --privileged. Each of these individually converts the sandbox into host access.
  • User namespace remapping so root inside is unprivileged outside.

Where the workload justifies it, use a stronger boundary: gVisor intercepts syscalls in userspace, Firecracker and Kata give a per-execution microVM with its own kernel. The cost is startup latency — tens to low hundreds of milliseconds for a microVM — which is negligible against model inference. For agents executing untrusted code at any real volume, a microVM per execution is the configuration that lets you stop worrying about the kernel surface.

Network Egress Is the Control That Matters Most

If you do one thing, do this: give the sandbox no network access.

Most analysis and transformation tasks need no network at all. Inputs are provided; outputs are returned through the tool interface. Default to a network namespace with no route out, and treat any need for connectivity as an exception requiring a specific allow-listed destination through a proxy.

When egress is genuinely required — installing a package, calling a permitted API — route it through a forward proxy with an allow-list, block private ranges and link-local addresses at the network layer, and log every request. Package installation in particular should point at an internal mirror rather than the public index, which also addresses dependency confusion and typosquatting. See securing the agent supply chain.

The reasoning is simple. Kernel escape gets an attacker onto the host. No egress means they cannot tell anyone what they found or send anything out — and in practice the goal of nearly every real attack is exfiltration.

Nothing Reachable Should Be a Credential

Enumerate what the sandbox process can read and remove every credential from that set:

  • Environment variables. Start the process with an explicitly constructed minimal environment. Do not inherit the parent's.
  • Cloud instance metadata. Blocked at the network layer, plus IMDSv2 enforced. Not one or the other.
  • Kubernetes service account tokens. Set automountServiceAccountToken: false. Almost no sandbox needs Kubernetes API access, and the default is to mount it.
  • Mounted secrets and config maps. None, unless there is a named requirement.
  • Filesystem paths outside the scratch directory. Read-only root plus a scratch mount covers this.
  • Kernel keyrings and process credentials from other processes. One process tree per execution, no shared PID namespace.

The test is easy to run: execute code in your sandbox that dumps the environment, walks the filesystem, and attempts a metadata fetch. Whatever it returns is what an attacker gets. Run it as a CI check so a configuration regression is caught rather than discovered.

Bounded Execution and Untrusted Output

Ceilings on every dimension: wall-clock timeout, CPU quota via cgroups, memory limit with OOM kill, process and thread count (pids cgroup — this is what stops fork bombs), disk quota on scratch, and output size cap.

Then the part that gets forgotten: the sandbox's output is untrusted. It goes into the agent's context and often into a UI or a downstream system. Code that prints a crafted string is code that can attempt injection against the next component. Apply schema validation and contextual escaping at each consumption point — see insecure output handling.

Finally, one execution per sandbox instance, destroyed after. Reuse is where cross-task persistence lives, and it is a false economy: a fresh microVM costs less than an incident investigation.

Common questions

Is a hosted code-interpreter service safer than running my own?

Usually, for the isolation layer — providers invest heavily in it and run per-execution microVMs. What they cannot do for you is decide what data you send in and what network reachability you grant. Most incidents in hosted setups come from the integration: credentials passed as inputs, sensitive data sent for analysis, or results trusted without validation. Buying the sandbox does not outsource the data-flow decisions.

Can I use static analysis to reject dangerous code before running it?

As a defense-in-depth layer for obvious cases, yes — blocking imports of socket, subprocess, or os catches naive attempts and costs little. As a boundary, no. Dynamic languages make analysis undecidable in general, and the bypasses are short: __import__ with a constructed string, getattr chains, eval of decoded bytes. Rely on the runtime boundary and treat static checks as noise reduction.

How do I let an agent install packages without opening egress?

Point the package manager at an internal mirror that proxies a vetted subset, reachable through the forward proxy as a single allow-listed destination. Pre-build images containing the common dependency set so most executions need no installation at all. Where a package genuinely must be fetched at runtime, pin versions and verify hashes — an unpinned install is a supply-chain decision made by whoever controls the index at that moment.

Should generated code be reviewed by a human?

For code that will be committed, deployed, or run outside the sandbox, yes — that code is entering your production trust context and deserves the same review as any other change. For ephemeral analysis code that runs only inside a properly-restricted sandbox and whose output is validated, review is not the control that matters; the sandbox is. Distinguishing these two paths clearly is what keeps review effort where it counts. See securing AI coding agents.

How Praesidia approaches execution containment

Praesidia governs the code-execution tool as a connection with its own enumerated scope, guardrails, spend ceiling, and trust-level requirement, so execution capability is granted per agent rather than inherited broadly — and it can be revoked or gated for approval independently of the agent's other tools. Because credentials are held at the proxy and attached after policy evaluation, no credential is present in the agent's context to be embedded in generated code.

Outbound traffic from a governed connection is mediated and recorded, so egress from an execution surface is visible and destination-constrained rather than open by default. Every invocation is recorded with attribution and cost, which is what makes runaway executions attributable. See tool use safety and sandboxing agent actions and the Praesidia documentation.