Isolating Untrusted Code With Containers
Running agent-written code in a bare subprocess is not a sandbox. This teardown rebuilds it with real agent harness container isolation using Docker flags that actually hold.
def run_agent_code(code: str) -> str:
with open("/tmp/agent_snippet.py", "w") as f:
f.write(code)
result = subprocess.run(
["python", "/tmp/agent_snippet.py"],
capture_output=True, text=True, timeout=30,
)
return result.stdout + result.stderrHow do you let an AI agent run code it wrote itself without letting that code delete your database, read your secrets, or mine crypto on your dime? That's the question every team reaches eventually, usually right after the first time an agent runs something surprising. The answer is agent harness container isolation — putting a real operating-system boundary between the agent's code and everything you care about. This is a teardown: we start with the weak version almost everyone ships first, break down exactly why it fails, then rebuild it properly.
Running agent-generated code without isolation is like handing a stranger the keys to your house because they promised to only use the kitchen. Maybe they will. But "maybe" is not a security model, and the model writing your code was trained on the entire internet, injection payloads included.
Before: The Weak Prompt
Here's the isolation strategy most harnesses start with — a subprocess and a hope.
[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
,[object Object], ,[object Object],(,[object Object],, ,[object Object],) ,[object Object], f:
f.write(code)
result = subprocess.run(
[,[object Object],, ,[object Object],],
capture_output=,[object Object],, text=,[object Object],, timeout=,[object Object],,
)
,[object Object], result.stdout + result.stderrWhat this does: Writes the agent's code to a temp file and runs it with the system Python, capturing output under a 30-second timeout. It feels contained because it's a separate process. It is not contained in any meaningful sense — that process runs as your user, sees your filesystem, and reaches your network.
Why It Fails
The subprocess shares everything with the parent. It runs as the same user, which means it can read every file that user can read — your SSH keys, your cloud credentials, your source code. It inherits the environment, so any secret in an environment variable is right there in
os.environwhile True: x = x + "a" * 10**6I've seen this exact setup let agent-written code read a
~/.aws/credentials⚠️ Common mistake: Believing a subprocess is a sandbox. A subprocess is an isolation boundary for crashes, not for security. If the code inside it can read your files and reach your network, it isn't sandboxed — it's just running somewhere slightly to the left of your main program, with all the same power.
After: The Improved Prompt
Real isolation means the code runs in a container with its own filesystem, no host credentials, dropped Linux capabilities, and hard resource limits. Here's the rebuilt version using Docker.
[object Object], docker, tempfile, os
client = docker.from_env()
,[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
,[object Object], tempfile.TemporaryDirectory() ,[object Object], workdir:
,[object Object], ,[object Object],(os.path.join(workdir, ,[object Object],), ,[object Object],) ,[object Object], f:
f.write(code)
container = client.containers.run(
image=,[object Object],,
command=[,[object Object],, ,[object Object],],
volumes={workdir: {,[object Object],: ,[object Object],, ,[object Object],: ,[object Object],}},
network_mode=,[object Object],,
mem_limit=,[object Object],,
nano_cpus=,[object Object],, ,[object Object],
pids_limit=,[object Object],,
cap_drop=[,[object Object],],
security_opt=[,[object Object],],
read_only=,[object Object],,
user=,[object Object],, ,[object Object],
detach=,[object Object],,
)
,[object Object],:
result = container.wait(timeout=,[object Object],)
logs = container.logs().decode()
,[object Object],:
container.remove(force=,[object Object],)
,[object Object], logsWhat this does: Runs the agent's code in a throwaway container with no network (
network_mode="none"nobodyBreaking Down Each Element
Each flag closes a specific hole from the "before" version, so it's worth understanding what every one buys you.
network_mode="none"cap_drop=["ALL"]no-new-privilegesuser="65534:65534"volumes={workdir: {,[object Object],: ,[object Object],, ,[object Object],: ,[object Object],}}What this does: Mounts only the single temp directory containing the snippet, read-only. The container can read its own code and nothing else from your host. There is no path from inside the container to your home directory, because your home directory was never mounted.
mem_limitnano_cpuspids_limit⚡ Pro tip: Set
read_only=Truetmpfs/tmp/tmpVariations for Different Contexts
Not every environment can run Docker, and the isolation primitive changes with your platform.
For a data-science agent that needs numpy and pandas, bake those into a custom image instead of
python:3.12-slimpip installFor serverless or Kubernetes environments where the Docker socket isn't available, gVisor (
runscFor a quick local prototype where you just need some boundary today, even an unprivileged container with
--network none⚡ Pro tip: Whatever primitive you pick, make the container ephemeral — one container per execution, destroyed immediately after. A long-lived sandbox that runs many snippets lets one run's leftovers (a written file, a poisoned cache) affect the next. Fresh container every time is both simpler to reason about and safer.
⚠️ Common mistake: Mounting the Docker socket into the container to let the agent "manage its own containers." Handing
/var/run/docker.sockVerifying the Isolation Actually Holds
Configuring the flags is one thing; proving they work is another, and the gap between the two is where breaches live. Don't trust that agent harness container isolation is working because the config looks right — test it with code that deliberately tries to break out, and confirm each attempt fails.
PROBES = {
,[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], PROBES.items():
logs = run_agent_code(code)
,[object Object], ,[object Object], ,[object Object], logs ,[object Object], logs == ,[object Object],, ,[object Object],
,[object Object],(,[object Object],)What this does: Runs a battery of escape attempts — reaching the network, reading host files, forking without limit, dumping environment variables — and asserts each one fails inside the container. If any probe succeeds where it should fail, your isolation has a hole and the test tells you exactly which one. Run this in CI so a config regression can't ship silently.
⚡ Pro tip: Keep these probes in your test suite and run them on every change to your container config. Isolation settings are exactly the kind of thing a well-meaning "just add one mount" pull request quietly weakens. A failing probe in CI catches the regression before it reaches production, where you'd otherwise learn about it from an incident instead of a red build.
The probes double as documentation. A new engineer reading
test_isolationReal-World Fit
A code-education platform runs student-submitted and AI-generated solutions side by side in identical
network_mode="none"rm -rfSave and Reuse This
One more thing before you ship it: measure the startup cost. A cold container adds real latency per execution — often a couple hundred milliseconds to a second — and for an agent that runs many small snippets, that adds up fast. If it matters for your workload, keep a small warm pool of pre-started containers and hand snippets to whichever one is free, resetting it between uses. You keep the isolation and lose most of the cold-start tax. Just don't reuse a container that ran something suspicious — destroy those and start fresh.
The container-run wrapper above is the kind of thing you write once and reuse across every agent you build — the flags are fiddly, the failure modes are subtle, and you do not want to reconstruct them from memory under deadline. Keep your hardened
run_agent_codeContinue 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.
