Agent Sandboxing — Isolation Patterns for Safe Tool Execution
AI agents that execute code, run shell commands, or browse the web need isolation boundaries — here's what to sandbox, how to do it with Docker and cloud sandboxes, and what you can safely skip.
An agent that can call subprocess.run() or os.system() on your production host can do anything your process user can do. That includes reading environment variables, accessing other services on the same network, writing to shared filesystems, and exfiltrating data through outbound HTTP. The model that issued the command might be well-intentioned — but the tool outputs it acts on might not be. Prompt injection via a malicious web page or document is a real attack vector, not a theoretical one.
Sandboxing isn’t about not trusting your own agents. It’s about containing the damage radius when something goes wrong, whether that’s a prompt injection, a logic error, or a model hallucinating a destructive command.
stateDiagram-v2
[*] --> TaskReceived
TaskReceived --> SafetyCheck: Agent decides to use a tool
SafetyCheck --> BlockedTool: Tool on blocklist
SafetyCheck --> DirectExecution: Tool is safe\n(read-only API, pure function)
SafetyCheck --> SandboxedExecution: Tool needs isolation\n(code exec, shell, browser)
SandboxedExecution --> ResourceLimits: Apply CPU/memory/time caps
ResourceLimits --> NetworkPolicy: Apply network restrictions
NetworkPolicy --> FilesystemIsolation: Mount only needed paths
FilesystemIsolation --> Execute
Execute --> ResultValidation: Validate output\nbefore returning to agent
ResultValidation --> TaskReceived: Continue agent loop
BlockedTool --> TaskReceived: Return error to agent
DirectExecution --> TaskReceived: Return result
What Needs Sandboxing
Not everything an agent does requires isolation. The overhead of spinning up a container or cloud sandbox for every tool call will kill your latency budget. The decision tree is straightforward:
Sandbox required:
- Code execution (Python, JavaScript, bash, any shell command)
- File system writes, especially in shared paths
- Browser automation and web scraping
- Any tool that takes external user-supplied content as input
Safe to run directly:
- Read-only API calls to well-defined external services
- Pure functions with no side effects
- Retrieval from a vector database or search index
- Structured data lookups in your own systems (with appropriate auth)
The line is: does this tool have side effects that could propagate beyond the task? If yes, sandbox it.
Docker-Based Sandboxing
The most portable sandbox is a Docker container with explicit resource limits, a stripped network policy, and a read-only filesystem except for a designated scratch directory.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import docker
import tempfile
import os
from pathlib import Path
def run_in_sandbox(
code: str,
language: str = "python",
timeout_seconds: int = 30,
memory_mb: int = 256,
) -> dict:
client = docker.from_env()
with tempfile.TemporaryDirectory() as tmpdir:
# Write the code to a temp file
if language == "python":
code_file = Path(tmpdir) / "main.py"
image = "python:3.12-slim"
cmd = ["python", "/sandbox/main.py"]
elif language == "javascript":
code_file = Path(tmpdir) / "main.js"
image = "node:20-slim"
cmd = ["node", "/sandbox/main.js"]
else:
raise ValueError(f"Unsupported language: {language}")
code_file.write_text(code)
try:
container = client.containers.run(
image=image,
command=cmd,
volumes={tmpdir: {"bind": "/sandbox", "mode": "ro"}},
network_mode="none", # No network access
mem_limit=f"{memory_mb}m",
memswap_limit=f"{memory_mb}m", # No swap
cpu_period=100000,
cpu_quota=50000, # 50% of one CPU
read_only=True, # Read-only root filesystem
tmpfs={"/tmp": "size=64m"}, # Writable /tmp only
remove=True,
detach=False,
stdout=True,
stderr=True,
timeout=timeout_seconds,
security_opt=["no-new-privileges"],
cap_drop=["ALL"], # Drop all Linux capabilities
)
return {
"success": True,
"output": container.decode("utf-8"),
"error": None,
}
except docker.errors.ContainerError as e:
return {
"success": False,
"output": None,
"error": e.stderr.decode("utf-8") if e.stderr else str(e),
}
except Exception as e:
return {
"success": False,
"output": None,
"error": str(e),
}
The critical flags: network_mode="none" prevents outbound connections. read_only=True blocks writing to the container’s root filesystem. cap_drop=["ALL"] removes Linux capabilities including the ability to bind to privileged ports or modify network interfaces. no-new-privileges prevents privilege escalation via setuid binaries.
Filesystem Isolation
When an agent task requires file access, mount only what it needs and mount everything else as read-only.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# docker-compose.yml for a code-execution agent
services:
code-executor:
image: python:3.12-slim
volumes:
- ./workspace:/workspace:rw # Agent's working directory
- ./shared-libs:/libs:ro # Shared dependencies, read-only
- /etc/ssl/certs:/etc/ssl/certs:ro # SSL certs for HTTPS
tmpfs:
- /tmp:size=128m,mode=1777
read_only: true
networks:
- restricted
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
networks:
restricted:
driver: bridge
internal: true # No external network access
Never mount the host’s home directory, /var/run/docker.sock (this lets a container escape to the host by controlling Docker), or any path containing secrets or credentials. If the agent needs an API key to make an outbound call, inject it as an environment variable scoped to that specific call, not as a mounted credentials file.
Resource Limits
Without limits, a misbehaving agent can consume all available CPU, exhaust memory and crash the host, or run indefinitely. Hard limits on all three:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Resource limit config — tune per use case
SANDBOX_LIMITS = {
"code_execution": {
"timeout_seconds": 30,
"memory_mb": 256,
"cpu_cores": 0.5,
"max_output_bytes": 1_000_000, # 1MB output cap
},
"browser_automation": {
"timeout_seconds": 60,
"memory_mb": 1024,
"cpu_cores": 1.0,
"max_output_bytes": 5_000_000,
},
"shell_commands": {
"timeout_seconds": 15,
"memory_mb": 128,
"cpu_cores": 0.25,
"max_output_bytes": 512_000,
}
}
The output cap matters. An agent in a loop writing 10MB of output per iteration will exhaust your context window and your memory before you notice. Truncate at the tool layer, not at the agent layer.
Cloud Sandboxes: E2B and Daytona
For teams that don’t want to manage Docker infrastructure, managed cloud sandboxes are a practical alternative.
E2B (formerly e2b.dev) provides on-demand sandboxed Python environments via API. Each sandbox is an isolated VM that starts in under a second, can run arbitrary code, and is terminated automatically after use.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from e2b_code_interpreter import Sandbox
async def run_agent_code(code: str) -> dict:
async with Sandbox() as sandbox:
execution = await sandbox.run_code(code)
if execution.error:
return {
"success": False,
"error": execution.error.value,
"traceback": execution.error.traceback,
}
return {
"success": True,
"output": "\n".join(str(r) for r in execution.results),
"logs": execution.logs.stdout,
}
Daytona focuses on development environment sandboxes — more appropriate when your agent needs to clone a repo, install dependencies, run tests, and report results. The sandbox persists for the duration of a task and supports SSH access for interactive debugging.
The tradeoff against self-managed Docker: you’re paying per-sandbox-second, which is more expensive at high volume but cheaper than operating your own sandbox infrastructure at low volume. The break-even is roughly 10,000–20,000 sandbox runs per day depending on average duration.
What You Can Skip
Sandboxing everything by default is the safe instinct but the wrong default in practice. Overhead adds up:
- A Docker container cold-start adds 1-3 seconds per tool call
- A managed cloud sandbox adds 800ms-2s
- Container image pulls on first use can add 10-30 seconds
For tools that read from an internal API with proper authentication, that call doesn’t need a sandbox — the auth layer is the isolation boundary. For pure computation (parsing JSON, running a regex, formatting data), sandboxing adds cost with no security benefit.
Build a tool classification layer that routes tool calls to the right execution context — direct execution, local Docker, or cloud sandbox — based on the tool’s risk profile. The classification itself should be static (based on the tool definition) rather than dynamic (based on the model’s assessment of the current call). Trust the tool definition, not the model’s judgment about whether a particular invocation is safe.
Day 4 of 7. Previous: Semantic Caching