Sandbox for AI-Generated Code: Running Agent Code Safely
Eli Mernit8 min read
To run code an AI agent writes, start a fresh sandbox for each task, block or allowlist its outbound network, keep real credentials out of it, cap its runtime, and destroy it when the task ends. The sandbox is the boundary. If the model writes something harmful, or a prompt injection talks it into something, the damage stays inside a machine you were going to throw away.
from beam import Image, Sandbox, SandboxProcessError
def run_agent_code(code: str, timeout: int = 30) -> dict:
sb = Sandbox(
cpu=1,
memory="1Gi",
image=Image(python_version="python3.11").add_python_packages(["pandas"]),
block_network=True, # no outbound connections at all
keep_warm_seconds=120, # hard TTL for the whole sandbox
).create()
try:
proc = sb.process.run_code(code, blocking=False)
try:
exit_code = proc.wait(timeout=timeout)
except SandboxProcessError:
proc.kill() # wait() stops waiting but doesn't stop the process
return {"exit_code": None, "error": f"timed out after {timeout}s"}
return {
"exit_code": exit_code,
"stdout": proc.stdout.read(),
"stderr": proc.stderr.read(),
}
finally:
sb.terminate()Hand run_agent_code to your agent as a tool. Each call gets a new sandbox with no network, a two-minute ceiling on its life, and a 30-second limit on the code itself, and the sandbox is gone when the call returns.
What running AI-generated code safely involves
Running AI-generated code safely means assuming the code is hostile, even when the model means well. You have four things to protect: the host the code runs on, the secrets and networks it could reach, your compute budget, and the next task's data.
Each one maps to a control:
| Risk | What it looks like | Control |
|---|---|---|
| Host escape | Code exploits the kernel or runtime to reach the machine underneath | Strong isolation: a user-space kernel or a microVM |
| Exfiltration | A prompt injection tells the agent to post your files or keys to an outside server | Egress blocked or allowlisted; no real secrets inside |
| Resource abuse | An infinite loop, a fork bomb, or a crypto miner | CPU and memory limits, process timeouts, a sandbox TTL |
| State leaks | Files from one user's task show up in another's | One sandbox per task, terminated afterward |
exec() in your app server covers none of these. A plain Docker container on a shared host covers some of them, which is why most teams that start there move to a managed sandbox. The Docker sandbox alternatives post covers that move in more detail.
How to run agent-generated code in a sandbox
The pattern is the same on any platform: create, configure, execute with a limit, collect results, terminate. On Beam each of those is one SDK call, and the sandbox docs cover the full API.
Lock down outbound network access
Block the network unless the task needs it. Most data analysis, math and test runs don't, and with block_network=True the sandbox can't open outbound connections, except to the approved hosts of any brokered secrets you bind.
When the code does need to reach something, such as a package index or one API, use an allowlist instead. Set one or the other, since Beam's docs say not to combine an explicit allow_list with block_network=True:
sb = Sandbox(
allow_list=["pypi.org", "*.pythonhosted.org", "api.github.com"],
).create()Beam allowlists take up to 64 entries: hostnames, wildcard subdomains, bare IPs or CIDRs. Hostname rules match on TLS SNI for port 443 and the HTTP Host header for port 80. The proxy rejects private, loopback and link-local addresses even if you list them, so generated code can't probe your internal network or the cloud metadata endpoint. You can tighten the rules on a running sandbox with sb.update_network_permissions(block_network=True), for example after a setup step that installs packages.
Keep real credentials out of the sandbox
Never pass a real API key into the sandbox through env or a file. Anything the code can read, a prompt injection can tell it to print or send.
Beam's brokered secrets solve the case where the code must call an authenticated API. You create the secret with the hosts it's allowed to reach:
beam secret create OPENAI_API_KEY "$OPENAI_API_KEY" --host api.openai.comThen bind it to the sandbox:
sb = Sandbox(
allow_list=["api.openai.com"],
secrets=["OPENAI_API_KEY"],
).create()Inside the sandbox, OPENAI_API_KEY holds a placeholder. The key itself is substituted only in HTTPS headers on requests to api.openai.com. If the code prints the variable, or sends it to any other host, all it leaks is the placeholder. Sandboxes also don't inherit your Beam workspace token, so generated code can't use your account to start more machines.
Cap time, CPU and memory
Set limits at three levels. The sandbox's cpu and memory bound what a runaway process can use. keep_warm_seconds is a hard TTL for the whole sandbox (the default is 600 seconds), and it counts down whether or not commands are running. proc.wait(timeout=...) bounds how long you wait on one piece of code.
That last one has a catch: a wait timeout raises SandboxProcessError but leaves the process running. Call proc.kill() in the handler, as the example at the top does, or the loop keeps burning CPU until the TTL ends the sandbox.
Move files in and out deliberately
Agents often need to analyze an upload or return a chart. Write inputs in and read outputs back through the filesystem API instead of mounting anything broad:
sb.fs.upload_file("report.csv", "/workspace/report.csv")
sb.process.run_code(code, cwd="/workspace")
sb.fs.download_file("/workspace/chart.png", "chart.png")The sandbox filesystem is temporary. If you mount a Volume for a model cache or dataset, assume the generated code can read and change everything on it, and mount only data it's allowed to touch.
Use one sandbox per task and terminate it
Create a new sandbox for each task or each user session, and call sb.terminate() in a finally block. Reusing a sandbox across users is how one person's files end up in another's output. Sandboxes boot in seconds, so per-task creation costs little latency.
If setup is slow (installing packages, cloning a repo), build it into the Image or take a snapshot once and start each task from it. The snapshot docs cover both filesystem images and memory snapshots.
Containers, gVisor or microVMs for untrusted code
For code nobody has reviewed, you want more than a standard container. A standard container shares the host kernel, so one kernel exploit reaches the machine. gVisor puts a user-space kernel between the code and the host, and a microVM gives the code its own guest kernel behind a hardware virtualization boundary.
Beam sandboxes run in a container runtime by default, and Sandbox(use_vm=True) runs one in a microVM instead. Beta9, the open-source engine behind Beam, ships runc, gVisor and microVM runtimes, and its default worker configuration uses gVisor. MicroVM sandboxes are CPU-only for now, so GPU work runs in the default runtime.
Which to pick depends on who writes the prompts. For an internal agent that writes analysis code for your own team, the default runtime with the network blocked is a reasonable baseline. For a product where anyone on the internet can steer the agent, use use_vm=True. The microVM explainer covers how the isolation models differ.
Defending against prompt injection and exfiltration
Prompt injection is the main reason to sandbox code your own agent writes. A web page, an email or a file the agent reads can carry instructions, and the model may follow them. You can't reliably filter those instructions out, so limit what the code can do if the model obeys them.
Three controls do most of the work:
- No secrets worth stealing inside the sandbox. Brokered secrets mean the code only ever sees a placeholder.
- Nowhere to send data. With the network blocked or allowlisted, a script that tries to POST your files to an attacker's server fails to connect.
- Nothing left behind. A terminated sandbox can't be used for a second, slower attack.
Two gaps to know about. Preview URLs from expose_port() are public, so add authentication in the app before you expose anything that serves data. And an allowlisted host is still a channel: if the code can reach api.github.com with a write-scoped token, it can push data there. Scope tokens to the least the task needs.
What to look for in a sandbox for AI-generated code
Look for isolation stronger than a shared-kernel container, egress controls you can set per sandbox and change at runtime, a way to call APIs without exposing keys, and lifetimes short enough that cleanup is automatic. Then check the practical parts: boot time, SDK quality, and whether it runs the hardware your agent needs.
Here is how Beam handles each one:
- Per-sandbox egress policy.
block_network, a 64-entryallow_list, andupdate_network_permissions()on a running sandbox. - Brokered secrets. Real keys are substituted outside the code's reach, only for hosts you approve.
- A choice of runtime. The default container runtime, or a microVM with
use_vm=True. - GPUs on request.
Sandbox(gpu="RTX4090")attaches a GPU for code that trains or runs a model. CUDA libraries go in the image. - Per-millisecond billing. Sandboxes are billed only while running, at $0.0000375 per physical core per second and $0.0000064 per GiB of RAM per second. A 1-core, 8 GiB sandbox costs $0.319 per hour. See pricing.
- Python and TypeScript SDKs, plus an open-source engine you can self-host.
If your agent runs tools over MCP rather than raw code, the same controls apply. The MCP server sandbox guide shows that setup.
Beam vs E2B vs Modal vs Daytona for agent code execution
All four give you an isolated sandbox with outbound network controls, so the differences are in the details: how strong the default isolation is, how you write egress rules, and whether real credentials ever enter the sandbox.
| Beam | E2B | Modal | Daytona | |
|---|---|---|---|---|
| Default isolation | Container runtime (beta9 default: gVisor) | Linux VM | gVisor | Linux container |
| Stronger option | MicroVM with use_vm=True (CPU only) | Already a VM | Own VM with a separate kernel under KVM | VM sandboxes |
| Block all egress | block_network=True | allow_internet_access=False | block_network=True | networkBlockAll |
| Allowlist | Up to 64 hostnames, wildcards, IPs or CIDRs | allowOut and denyOut lists of IPs, CIDRs or domains | CIDR allowlist, plus a domain allowlist in beta (TLS on 443 only) | Up to 10 IPv4 CIDRs, plus up to 100 domains |
| Change rules while running | update_network_permissions() | update_network(), which replaces the whole config | Experimental API | Tiers 3 and 4 only |
| Keys kept out of the sandbox | Brokered secrets, substituted in HTTPS headers to approved hosts | Header injection rules in public beta, 10 domains per sandbox | Not covered in its networking docs | Outbound proxy URL; no injection documented |
Where the others are ahead: E2B starts every sandbox as a VM, so you get VM isolation without a flag, and its credential rules can mint workload-identity tokens. Daytona allows more domain entries and documents GPU sandboxes with up to 8 GPUs.
Beam's edge for this use case is combining a large mixed allowlist with brokered secrets that are generally available, so a sandbox can call one API with a real key while the code itself never sees it. For a wider look at the field, see E2B alternatives.
FAQ
Is Docker enough to run AI-generated code?
Not for code from untrusted prompts. A standard Docker container shares the host kernel, and its default network setup allows outbound traffic. It's a reasonable start for code you review, but for agent output that a prompt injection could shape, use a user-space kernel like gVisor or a microVM, with egress blocked.
Can sandboxed code call the OpenAI API or another LLM?
Yes. Allowlist the API host and bind the key as a brokered secret. The code sees a placeholder, and Beam substitutes the real key only in HTTPS headers sent to that host.
How long can a sandbox run?
As long as its TTL allows. keep_warm_seconds defaults to 600 seconds, you can reset it with sb.update_ttl(), and -1 keeps the sandbox running until you terminate it. For a machine that keeps its disk between sessions, use a VM instead.
Do files persist after the sandbox ends?
No. The sandbox filesystem is temporary. Download what you need before you terminate it, mount a Volume for shared data, or snapshot the filesystem to reuse it as an image.
Can AI-generated code run on a GPU?
Yes. Pass gpu="RTX4090" or another supported GPU when you create the sandbox, and include the CUDA libraries in the image. GPU sandboxes use the default runtime, since microVM sandboxes are CPU-only for now.
Are exposed sandbox URLs private?
No. URLs from expose_port() are public HTTPS endpoints, and blocking outbound traffic doesn't change that. Add authentication inside the app you serve.
Run your agent's code in a Beam sandbox
Give your agent a tool that runs its code in a fresh, network-blocked sandbox and returns the output. Create a Beam sandbox and start from the example at the top of this page.
