Sandboxing Code Execution: What a `run_code` Tool Actually Needs
One tool replaces dozens — and hands a model that reads untrusted text a general-purpose mechanism for doing anything the process can do. Four boundaries, four isolation levels, and the one people leave open.
Giving an agent the ability to execute code is the single largest capability jump available, and the single largest expansion of what can go wrong. One tool replaces dozens: instead of building a data-analysis tool, a file-conversion tool, and a calculation tool, you give it an interpreter.
That interpreter is also a general-purpose mechanism for doing anything the process can do, driven by a model that reads untrusted text.
The four boundaries
A useful sandbox controls four things. Missing any one leaves a gap the others don't cover.
Filesystem. What can be read and written. The default of "everything the process user can touch" includes credentials, SSH keys, and other tenants' data on a shared host.
Network. Whether outbound connections are possible and to where. This is the exfiltration channel, and it's the one most often left open because blocking it breaks package installation and feels inconvenient.
Compute. CPU time, memory, wall clock, process count. Without limits, an accidental infinite loop or a fork bomb takes down the host — and "accidental" is the common case, not the malicious one.
Privilege. What the process itself can do: no root, no capabilities, no access to the container runtime.
⚠️ The order to worry about them: network first, because exfiltration is silent and irreversible; filesystem second; then compute and privilege.
Isolation levels, in ascending strength
Same process, restricted interpreter. Blocking imports, patching builtins. Do not use this as a security boundary. Every language with introspection has a way around it, and the escapes are well documented. Acceptable as a guard against accidents, never against intent.
Separate process, OS-level limits. Resource limits, a dedicated unprivileged user, restricted syscalls. Real protection against accidents and casual misuse; a determined escape is still plausible.
Container per execution. No network unless granted, read-only root filesystem, one writable scratch mount, dropped capabilities, memory and CPU limits, hard timeout, destroyed after use. This is the practical default for production agent code execution, and it's what most managed sandboxes provide.
MicroVM or hardware-isolated. A genuine kernel boundary. Appropriate when running code derived from wholly untrusted input, or in a multi-tenant product where a container escape is a company-ending event.
Pick by what happens if the boundary fails, not by what feels proportionate to the agent's intended job.
Getting data in and out
The sandbox is useless if it can't see the data — and the way data gets in is where isolation quietly leaks.
✅ Mount inputs read-only, scoped to the run. Not the whole data directory; the specific files this task needs.
✅ Return outputs by a defined channel. A designated output directory, or stdout with a size cap. An unbounded return blows your context and can be used to smuggle out whatever was readable.
❌ Never pass credentials in. If the code needs to call an API, put the credential in a proxy the sandbox can reach, with the credential added outside — so the code can make the call and cannot read the secret.
Dependencies are the hard part
Code that needs packages is where sandboxes get relaxed, because a network-isolated sandbox can't install anything.
The approach that works: pre-build images with the common libraries, and treat the set as a curated allowlist. If a task genuinely needs something new, that's a request to update the image — a slow, reviewed path — rather than a live pip install from inside the sandbox.
An agent that can install arbitrary packages at runtime has a general-purpose network channel and an arbitrary-code-execution supply chain, which is most of what the sandbox was for.
🔍 What to log
Code execution needs an audit trail beyond the usual trace:
- The exact code, verbatim. Not a summary of what it did.
- Every file read and written, with paths.
- Every network attempt, allowed or blocked. Blocked attempts are a signal worth alerting on — they're rarely accidental.
- Resource consumption and exit status, including timeouts.
Blocked-egress attempts in particular deserve attention: legitimate analysis code has little reason to reach the internet, so a spike usually means either an injection succeeded in influencing the generated code, or someone is probing.
The cheaper alternative worth considering first
Before granting general execution, check whether a constrained evaluator does the job. A sandboxed expression evaluator for arithmetic, a query interface for data questions, a fixed set of transformations — each covers a large share of what teams reach for run_code to accomplish, with a vastly smaller attack surface.
General code execution is the right answer for genuinely open-ended analysis. It's an oversized answer for "the agent needs to do arithmetic reliably."
The takeaway
Treat run_code as remote code execution driven by a model that reads untrusted text, because that's what it is. Default to a container per execution with no network, a read-only root, scoped read-only inputs, hard resource limits, and destruction afterwards. Keep credentials outside and reach them through a proxy, curate dependencies in the image, and log the code and every blocked egress attempt. And check first whether a narrower tool would have done the job.