Containerized Sandboxes for Code-Executing Agent Tools
When an agent can run model-written code, that code must not share the host's filesystem, network, or credentials.
Containers (or stronger: microVMs) give you a practical isolation boundary you can automate on each tool call.
Summary
Run untrusted agent code in a disposable container with a read-only root filesystem, no host network (or tightly allowlisted egress), dropped capabilities, resource limits, and a single mounted work directory that holds only the job inputs.
Recipe
1. Separate the agent host from the executor
The process that talks to the model holds API keys. The process that runs user/model code must not.
2. Build a minimal execution image
Use a slim base, non-root user, no compilers or cloud CLIs unless required, and pin digests in production (verify tags at build).
3. Default-deny network
Start with --network none. Add egress only for documented package or API needs, never "whatever the snippet imports."
4. Mount the least filesystem
Bind-mount a per-job temp directory as the working dir. Do not mount $HOME, Docker socket, cloud metadata paths, or secret volumes.
5. Drop privileges and capabilities
Non-root user, read-only root FS, drop NET_RAW and other caps, no privilege escalation.
6. Cap CPU, memory, PIDs, and wall time
cgroup limits plus a hard kill on timeout stop fork bombs and runaway loops.
7. Capture bounded stdout/stderr
Truncate output before it re-enters the model context. Redact secret-shaped strings if inputs could contain them.
8. Destroy the container after each job
Ephemeral containers prevent persistence across tenants and turns.
Working Example
This pattern shells out to Docker from a tool handler. Swap Podman or a job API as needed; the security shape stays the same.
import subprocess
import tempfile
import textwrap
from pathlib import Path
def run_in_container(source: str, *, timeout_s: int = 5) -> dict:
with tempfile.TemporaryDirectory() as td:
work = Path(td)
(work / "main.py").write_text(textwrap.dedent(source), encoding="utf-8")
cmd = [
"docker", "run", "--rm",
"--network", "none",
"--read-only",
"--tmpfs", "/tmp:rw,noexec,nosuid,size=16m",
"--user", "65534:65534",
"--cap-drop", "ALL",
"--security-opt", "no-new-privileges",
"--memory", "256m",
"--cpus", "0.5",
"--pids-limit", "64",
"-v", f"{work}:/work:ro",
"-w", "/work",
"python:3.12-slim", # pin digest in prod; verify at build
"python", "/work/main.py",
]
try:
proc = subprocess.run(
cmd, capture_output=True, text=True, timeout=timeout_s + 2
)
except subprocess.TimeoutExpired:
return {"ok": False, "error": "timeout"}
out = (proc.stdout or "")[:8_000]
err = (proc.stderr or "")[:2_000]
return {
"ok": proc.returncode == 0,
"stdout": out,
"stderr": err,
"code": proc.returncode,
}
print(run_in_container("print(sum(range(10)))"))Wire run_in_container behind your run_python_sandboxed tool; the model only sees the JSON observation, not Docker.
Deep Dive
Why containers beat in-process exec
In-process restricted Python is easy to demo and hard to seal. C extensions, bytecode tricks, and host library bugs share an address space with your secrets.
A container moves the trust boundary to the OS and runtime. Escape risk is not zero, but it is a different class of problem with standard hardening playbooks.
Image design
| Choice | Why |
|---|---|
| Slim / distroless-style | Fewer packages to abuse |
| Non-root | Limits host mapping damage |
| No Docker CLI inside | Avoid nested breakout helpers |
| Pinned digest | Reproducible, less supply surprise |
| Preinstalled deps only | Runtime pip install needs network and is risky |
If snippets need third-party libraries, prebake them into the image or use a reviewed dependency allowlist with offline wheels.
Filesystem policy
- Read-only root forces writes into tmpfs or an explicit data mount.
- Per-job directory isolates tenants; never reuse a dirty workspace.
- Optional output mount can be writable if the tool must return files; scan and size-cap them.
Network policy
--network none is the correct default for pure compute (stats, transforms, puzzles).
If code must call an internal API, prefer:
- Host-side tool does the HTTP call with scoped credentials, or
- Container egress via a proxy that allowlists destinations and strips secrets.
Never pass the model's OpenRouter/OpenAI key into the sandbox "so the code can call the LLM."
gVisor, Kata, Firecracker
For multi-tenant SaaS code execution, many teams add a stronger runtime (gVisor, Kata Containers, Firecracker microVMs). Containers alone may be enough for trusted-employee agents; hostile multi-tenant usually needs more.
Observability
Log: job id, image digest, timeout, exit code, bytes of stdout, policy version. Do not log full customer source if policy forbids it; hash or sample instead.
Framework placement
LangGraph, CrewAI, OpenAI Agents SDK, and custom hosts should treat container exec as an ordinary tool with a schema. Isolation is not a framework feature you "turn on"; it is infrastructure you own.
Gotchas
- Mounting the Docker socket - Equivalent to root on the host. Never expose it to agent code or agent-writable paths.
--network host- Defeats isolation for convenience. Avoid.- Sharing one long-lived container - Cross-job residue, privilege persistence, noisy neighbors.
- Root in container with bind mounts - UIDs can still damage mounted host files depending on setup.
- Unbounded output - Multi-megabyte prints blow context and cost.
- Pulling images on the hot path - Supply chain and latency risk; pre-pull pinned images.
- Assuming
python:slimis safe forever - Rebuild and patch on a schedule; verify base image policy at build.
Alternatives
| Approach | Strength | Weakness | Best fit |
|---|---|---|---|
| In-process restricted exec | Simple | Weak isolation | Local demos only |
| Docker/Podman per job | Practical default | Escape surface remains | Most product agents |
| gVisor / Kata / microVM | Stronger isolation | Ops complexity | Multi-tenant SaaS |
| Remote serverless sandbox products | Managed lifecycle | Vendor limits, data path | Teams without container ops |
| No code exec (structured tools only) | Smallest risk | Less flexible | Many business agents |
FAQs
Is Docker required?
No. Any isolator with resource limits, filesystem jail, and network policy works. Docker is a common lowest common denominator.
Can the agent install packages inside the container?
Prefer prebaked images. Ad hoc installs need network and can pull malware. If you allow installs, pin indexes, allowlist packages, and still drop credentials.
How long should a job live?
Seconds for typical snippets. Hard-kill above your product SLA. Longer jobs need queues and clearer abuse controls.
What user should run inside the container?
A dedicated non-root UID with no sudo and no writable sensitive mounts.
How do I return plots or files to the agent?
Write to a size-capped output directory, copy out from the host after exit, virus-scan if untrusted, then summarize or attach metadata - not raw host paths.
Does read-only root break legitimate code?
Sometimes. Provide /tmp tmpfs and document that only /work (or similar) is available.
Should unit tests run in the same sandbox?
Yes for the executor path. Negative tests (network, file escape, timeout) belong in CI.
How does this relate to least privilege?
The container is how you enforce least privilege for the code tool's OS powers. Tool schema still limits when the model may call it.
What about browsers and GUI agents?
Same ideas: dedicated container/VM, separate profile, no host cookie jar, limited egress. Browser tooling is high risk and needs its own threat model.
Can I reuse warm pools for speed?
Yes if you reset state thoroughly (new filesystem layer, cleared env, no residual processes) and never mix tenants in a dirty pool.
Related
- Agent Security Basics - first restricted exec pattern
- The Principle of Least Privilege Applied to Agent Tools - design principle
- Network Egress Controls for Agents That Call External Tools - when network is required
- Scoping API Keys and Credentials Passed to an Agent - keep secrets off the sandbox
- Never Grant Blanket Shell Access in Production: What to Do Instead - prefer structured tools
- Security Checklist Before Shipping an Agent to Production
- Agent Security Best Practices
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.