How to Sandbox Code Execution in Your Agent Harness
An agent code execution sandbox has to stop more than file deletion. Here's how to block network exfiltration, strip credentials, and cap resources safely.
def run_code_tool(code: str):
scope = {}
exec(code, scope) # runs in this process, full privileges
return scope.get("result", "done")Ask ten engineers what a code sandbox protects against and nine will say "the model deleting my files." That instinct points at the wrong threat. The quieter, more common breach is a script that quietly makes a network call — reading a secret from an environment variable and posting it somewhere you'll never see. An agent code execution sandbox that only guards the filesystem leaves the front door wide open while bolting the back one. Let's tear down a naive executor and rebuild it around the threats that actually happen.
Before: The Naive exec()
exec()Here's the version almost everyone writes first, because it works in the demo:
[object Object], ,[object Object],(,[object Object],):
scope = {}
,[object Object],(code, scope) ,[object Object],
,[object Object], scope.get(,[object Object],, ,[object Object],)What this does: it takes whatever Python the model wrote and runs it directly inside your own process, with your permissions, your environment variables, and your network. It returns whatever the code stashed in
resultWhy It Fails
The failure isn't hypothetical, and it isn't only about malicious models. A perfectly well-meaning model, asked to "check the config," might write code that reads
os.environexec()Four specific holes make the naive version unusable in anything real:
- Network egress. The code can reach the internet and your internal network. This is the exfiltration path most people forget, and it's the one that leaks secrets.
- Ambient credentials. Environment variables, mounted cloud credentials, and open database connections are all visible to . The model didn't need to break in — you handed it the keys.
exec() - No resource ceiling. A or a runaway allocation freezes or crashes your host. LLMs write infinite loops more often than you'd hope.
while True: - Shared process state. Code running via can reach into your harness's own variables and corrupt the run itself.
exec()
⚠️ Common mistake: Treating a code sandbox as purely a filesystem problem. Blocking file writes while leaving network access open is the security equivalent of locking the windows and propping the door. The three things that matter most — no network, no ambient credentials, hard resource limits — have nothing to do with the filesystem at all.
After: The Sandboxed Executor
The fix moves execution out of your process and into a disposable, locked-down environment. A container is the pragmatic default:
[object Object], subprocess, tempfile, os
,[object Object], ,[object Object],(,[object Object],):
,[object Object], tempfile.TemporaryDirectory() ,[object Object], work:
path = os.path.join(work, ,[object Object],)
,[object Object], ,[object Object],(path, ,[object Object],) ,[object Object], f:
f.write(code)
,[object Object],:
out = subprocess.run(
[,[object Object],, ,[object Object],, ,[object Object],,
,[object Object],, ,[object Object],, ,[object Object],
,[object Object],, ,[object Object],, ,[object Object],
,[object Object],, ,[object Object],, ,[object Object],
,[object Object],, ,[object Object],, ,[object Object],
,[object Object],, ,[object Object],
,[object Object],, ,[object Object],,
,[object Object],, ,[object Object],, ,[object Object],
,[object Object],, ,[object Object],, ,[object Object],],
capture_output=,[object Object],, text=,[object Object],, timeout=timeout)
,[object Object], (out.stdout ,[object Object], out.stderr)[:,[object Object],]
,[object Object], subprocess.TimeoutExpired:
,[object Object], ,[object Object],What this does: it writes the model's code to a throwaway directory, runs it inside a container with no network, capped memory and CPU, a process-count limit, a read-only root filesystem, and one small writable scratch space — then kills it on a timeout. Every credential in your host process is invisible to it. When the container exits, everything the code touched vanishes with it.
⚡ Pro tip:
--network noneBreaking Down the Sandbox Layers
Each flag maps to a specific threat, and understanding the mapping lets you tune the sandbox for your situation instead of copying it blindly.
Isolation of credentials comes from running in a fresh container that never inherited your environment. This is why "run it in a subprocess" isn't enough — a subprocess still sees your environment variables. The container starts clean.
Resource ceilings —
--memory--cpus--pids-limitFilesystem containment comes from
--read-onlyPrivilege containment comes from
no-new-privilegessetuidFor higher-assurance needs, containers aren't the ceiling. Micro-VMs like Firecracker or a gVisor runtime give you kernel-level isolation instead of shared-kernel namespaces — heavier to operate, but the right call when the code is truly untrusted.
⚡ Pro tip: Pin the image to a digest, not the
:latestpython:3.12-slim@sha256:...Testing That the Sandbox Actually Holds
A sandbox you haven't attacked is a sandbox you don't trust yet. The difference between an agent code execution sandbox that works and one that only looks like it works shows up when you point adversarial code at it on purpose. Write the escape attempts yourself, before a model writes them by accident:
ESCAPE_TESTS = {
,[object Object],: ,[object Object],,
,[object Object],: ,[object Object],,
,[object Object],: ,[object Object],,
,[object Object],: ,[object Object],,
,[object Object],: ,[object Object],,
}
,[object Object], ,[object Object],(,[object Object],):
,[object Object], name, code ,[object Object], ESCAPE_TESTS.items():
out = runner(code)
blocked = ,[object Object], ,[object Object], out ,[object Object], ,[object Object], ,[object Object], out.lower() ,[object Object], ,[object Object], ,[object Object], out
,[object Object],(,[object Object],)What this does: it runs five representative attacks — a network call, an environment dump, a fork bomb, a CPU spin, and a write outside the scratch dir — and reports which ones the sandbox stopped. Every line should print
BLOCKEDESCAPEDRun this audit in your CI pipeline, not just once by hand. Base images change, Docker flags get dropped in a refactor, and a sandbox that held last month can quietly spring a leak. An automated audit catches the regression the day it happens rather than the day it matters.
Three teams that gained real confidence from this:
- A DevOps engineer wires the audit into the deploy pipeline, so any change to the container config that reopens network egress fails the build before it ships.
- A fintech platform engineer extends the escape tests with attempts to reach the internal metadata endpoint that leaks cloud credentials, proving that specific exfiltration path is closed.
- A university research group running student-submitted agents runs the full audit per submission, treating every unaudited sandbox as untrusted by policy.
⚡ Pro tip: Keep the escape tests in the same repo as the sandbox and treat a new escape technique like a new bug — add a test that reproduces it, then fix until the test goes green. Over time your
ESCAPE_TESTSVariations for Different Threat Models
The right sandbox depends on how much you trust the code's source, and matching effort to threat keeps you from over-building.
- A data science team running model-written pandas snippets against non-sensitive sample data can stay on the container setup above — no network, capped resources, disposable. The threat is mostly runaway loops, not exfiltration.
- A fintech platform engineer whose agent generates code that touches anything near real customer data should move to micro-VM isolation and route the one permitted outbound call through an audited proxy. Here the exfiltration threat is the whole game.
- A security research group analyzing genuinely untrusted code treats every execution as hostile: micro-VM, zero network, no shared kernel, and a fresh VM per run so nothing carries between executions.
Same core idea, three levels of paranoia — chosen by what happens if the containment fails, not by what feels rigorous.
There's a cost dimension worth weighing too. A fresh micro-VM per execution is the safest option and also the slowest to start and the most expensive to run at scale. If your agent runs thousands of snippets a day and the code comes from a source you partly trust, a warm pool of pre-started containers gives you most of the isolation at a fraction of the per-run cost. The right answer balances the blast radius of an escape against the throughput you actually need — a research tool analyzing malware and a data agent running vetted pandas snippets sit at opposite ends of that trade, and copying one's setup onto the other either wastes money or takes on risk you didn't mean to.
Save and Reuse This
The move that makes an agent code execution sandbox actually safe is separating two decisions your naive
exec()There's one more piece worth keeping: the system prompt and tool description that tell the model what the sandbox can and can't do. A model that knows it has no network access stops writing code that assumes network access, which cuts your failed-run count sharply. Those hard-won descriptions — "you run in a sandbox with no internet, 256MB RAM, and a 10-second limit" — belong in a prompt library like PromptABCD, tagged to the sandbox they describe, so every new code agent inherits the guardrails you already worked out instead of learning them by crashing.
Continue Reading
Save the prompts from this post
PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.
