How to Sandbox AI Agents in 2026
An AI agent is, stripped of the mystique, a loop: ask a model what to do next, do it, feed the result back, repeat. In 2026 the 'do it' step almost always means executing something the model generated — a Python snippet, a shell command, a tool call that hits an API, a file it decided to write. That's the whole reason agents are useful, and it's also the whole reason they're dangerous. Somewhere in that loop, code you did not write and cannot review runs on a machine you care about. The question this post answers is the practical one: how do you put a real boundary around that in 2026, without making your agent slow or your architecture a science project.
This is a how-to, not a product roundup — if you want the honest field of sandbox products, that's /blog/best-ai-agent-sandboxes-2026, and the ranked shortlist is /blog/top-ai-agent-sandbox-platforms-2026. Here I want to walk the actual decisions: what you're defending against, the ladder of isolation options and where each one honestly sits, and a concrete architecture — per-run microVM, scoped credentials, egress control, teardown — with code you can adapt. I'm Ajay; I build PandaStack, a Firecracker microVM platform, so I'll use it for the worked examples, but the shape of the architecture is what matters and it applies whatever substrate you land on.
Step 1: get the threat model right
You can't sandbox well until you're honest about what can go wrong, and with agents it's a broader set than 'the model writes rm -rf' (though it does that too). Three distinct failure modes matter, and they need different parts of the boundary.
- The model is wrong. Most agent damage isn't malice — it's a capable model confidently doing the wrong thing: deleting the wrong files, running a destructive migration, hammering an API, burning your budget in a loop. The boundary has to contain competent mistakes, not just attacks.
- The model is steered. Prompt injection is a first-class threat now: a web page the agent reads, a document it parses, a tool result it ingests can contain instructions that redirect it. Your agent's own inputs are attacker-reachable, so 'the code the agent runs' can be code an attacker talked it into running.
- The code is exploited. The tools an agent invokes — parsers, package installers, headless browsers — have their own vulnerabilities. A malicious PDF or a poisoned dependency turns a tool call into remote code execution regardless of what the model intended.
- The blast radius is whatever the run can touch. In every case above, the damage is bounded by what the execution environment holds and can reach: its credentials, its filesystem, its network. That's the thing you actually control, so that's what the sandbox is for.
Step 2: pick your isolation boundary honestly
There's a ladder of ways to run untrusted code, and the honest version of this section is that they're not interchangeable — each rung has a real boundary strength and a real cost. Match the rung to your threat model.
- Same process (eval / a subprocess on your host) — no boundary. The code has your credentials, your files, your network. Fine for a demo with a model you fully trust and nothing to lose; catastrophic for anything real. This is the rung most agents start on by accident.
- A container — better, and still a shared host kernel. Namespaces, cgroups, dropped capabilities, and seccomp shrink the blast radius meaningfully, but a kernel local-privilege-escalation or a container escape crosses the boundary, and mounted secrets still leak. Reasonable for semi-trusted code; a known risk for genuinely untrusted, multi-tenant, or prompt-injectable agents.
- A userspace-kernel sandbox (gVisor-style) — a big reduction in host-kernel attack surface without a full VM, at a syscall-compatibility and overhead cost. A strong middle rung for defense-in-depth.
- A microVM (Firecracker and platforms on it) — a separate guest kernel under KVM hardware virtualization. The untrusted code reaches its own kernel, and an escape means beating the hypervisor, not a shared syscall surface. This is the rung that matches 'assume the code is hostile,' which is the right assumption for agent code in 2026.
- A compile-to-WASM sandbox — capability-based and very light, but only for code you can target to WASM. Great for plugins you control the build of; not a fit for 'run whatever Python the model wrote.'
For an agent running model-generated, prompt-injectable code with access to real tools and data, the honest answer is a hardware boundary per run — a microVM. The classic objection was that VMs are slow to start, which mattered because the sandbox sits inside your agent's inner loop and you create one constantly. In 2026 that objection is largely dead: snapshot-restore means a VM create is a restore of an already-booted machine, not a cold boot. On PandaStack the restore step is around 49ms, an end-to-end create is p50 179ms and p99 about 203ms, and a true cold boot happens only on the first spawn of a template (around 3s). Hardware isolation at roughly container-grade latency is what makes per-run VMs practical.
Step 3: the per-run sandbox architecture
Boundary strength is necessary but not sufficient — a perfectly isolated VM handed your root credentials with open internet is a well-contained way to leak everything. The architecture that actually holds has four parts beyond the isolation itself. Think of them as the four walls.
- One disposable environment per run. Don't reuse a warm worker across agent steps or across users — a fresh boundary per execution means nothing carries over: no cached credential, no leftover file, no state one run can inherit from another. This is the single most important architectural choice, because it turns 'a compromise' into 'a compromise of one thing you were about to throw away.'
- No ambient credentials; inject only what the run needs, scoped and short-lived. The environment starts with nothing — no cloud role, no mounted secrets, no keys in the environment. If a run legitimately needs to touch a resource, mint a per-run credential scoped to exactly that resource, valid for minutes, injected as a file a specific process reads. And block the metadata endpoint (169.254.169.254), which is the most common way sandboxed code escalates to the host's role.
- Deny-by-default egress. Give the run its own network namespace and allowlist only the endpoints its task requires. This is what turns 'the agent read some data' into 'the agent can't send the data anywhere' — the load-bearing control against both exfiltration and prompt-injection-driven callbacks.
- A hard time and resource cap, plus teardown. A ttl and an exec timeout reap a runaway loop or a wedged tool call so it kills one environment, not your fleet. Teardown at the end destroys everything the run touched. The cap is your circuit breaker; teardown is your cleanup, and it's free.
Step 4: a worked example (the agent's exec step)
Here's the shape of the single most important function in a sandboxed agent: the one that runs a step of model-generated code. Fresh VM, egress locked, no ambient creds, scoped creds only if the step needs them, tagged for audit, destroyed at the end.
from pandastack import Sandbox
def run_agent_step(code: str, run_id: str, session_id: str,
scoped_creds: dict | None = None) -> dict:
"""Execute ONE step of model-generated code in its own microVM.
Fresh boundary, no ambient creds, deny-by-default egress, scoped
short-lived creds only if the step needs them, destroyed when done."""
with Sandbox.create(
template="code-interpreter",
ttl_seconds=120, # backstop: a runaway step reaps itself
metadata={"run_id": run_id, "session": session_id}, # audit trail
# Create with egress allowlisted to ONLY this step's endpoints (or
# none). Metadata endpoint (169.254.169.254) blocked.
) as sbx:
# No host creds live in this guest. If the step genuinely needs to
# touch a resource, inject a SCOPED, SHORT-LIVED credential as a file.
if scoped_creds:
sbx.filesystem.write("/run/creds.json", _to_json(scoped_creds))
# The model's code. We never trust it -- it runs behind a hardware
# boundary, not next to the agent's own credentials.
sbx.filesystem.write("/run/step.py", code)
# timeout_seconds is the circuit breaker; it kills THIS vm only.
result = sbx.exec("python3 /run/step.py", timeout_seconds=90)
return {
"run": run_id,
"exit_code": result.exit_code,
"stdout": result.stdout, # feed back into the agent loop
"stderr": result.stderr,
}
# VM gone here: the code, any scoped cred, cached tokens, and every byte
# the step touched are destroyed. The next step starts from nothing.The agent loop calls this once per step and feeds `stdout` back to the model as the observation. Note the ergonomics you want from whatever substrate you pick: create a machine, write files into it, exec with a timeout, read results, and have it torn down for you. Anything that makes those four operations awkward will make your agent code awkward, because your agent does them constantly.
Step 5: handle the stateful-agent case
Pure per-step VMs are cleanest, but many agents need continuity — a working directory, an installed dependency, a running dev server — to persist across steps within a task. You don't have to choose between 'isolated' and 'stateful.' Two patterns cover it.
- One long-lived sandbox per agent session, torn down at session end. Give a task its own microVM for its whole run so state persists across steps, and destroy it when the task finishes. Isolation is per-session instead of per-step; the hard rule is one session per VM — the instant you reuse a session's VM for a different user or task, you've rebuilt the shared-worker leak. Mark it persistent so an idle reaper doesn't kill mid-task, and still don't bake long-lived credentials into it.
- Snapshot-and-fork for warm, isolated starts. Snapshot a VM that already has the toolchain, dependencies, and repo warmed, then fork it per task or per branch of exploration. A same-host fork lands in 400–750ms (cross-host 1.2–3.5s) with copy-on-write memory and disk, so each run gets a private, disposable machine that starts from a known-good state without a full setup — and still dies, credentials and all, when it's done. This is also how you run best-of-N: fork the same warm state into several isolated attempts.
The 2026 sandboxing checklist
- Never run agent code in your own process or on your host directly — that's not a sandbox, it's a loaded gun with a progress bar.
- Choose a boundary that matches 'assume the code is hostile' — a microVM for untrusted, prompt-injectable agent code; a container only for semi-trusted code you've reasoned about.
- Fresh environment per run (or per session, torn down at the end) — no reused warm workers carrying state across users or tasks.
- No ambient credentials; inject per-run, scoped, short-lived creds as files; block the metadata endpoint.
- Deny-by-default egress; allowlist only the endpoints each run needs — this is your anti-exfiltration and anti-injection-callback wall.
- Hard ttl + exec timeout so runaways reap themselves; teardown destroys everything.
- Tag every run (session/run id) for an audit trail, and keep create latency low enough (snapshot-restore) that the boundary is the default, not a special case.
Putting it together
Sandboxing an AI agent in 2026 isn't a single trick; it's a small, boring architecture applied consistently. Get the threat model right — the model is wrong, steered, or exploited, and the blast radius is whatever the run can touch. Pick a boundary that matches 'assume hostile,' which for real agent code means a hardware-isolated microVM per run, made practical by snapshot-restore. Then put the four walls around it: one disposable environment per run, no ambient credentials, deny-by-default egress, and a hard cap with teardown. Handle stateful agents with per-session VMs or snapshot-and-fork so you never trade isolation for continuity. Do that and the inevitable bad moment — the wrong migration, the injected instruction, the poisoned dependency — lands in a disposable machine with scoped keys, no egress, and a short fuse. Which is the entire job: not a perfect agent, just a small blast radius.
Frequently asked questions
Why do I need to sandbox an AI agent at all — can't I just trust a good model?
No, and not because the model is bad — because trust isn't the failure mode you're defending against. A capable model still confidently does wrong things (deletes the wrong files, runs a destructive migration, loops through your budget), can be steered by prompt injection hidden in a web page, document, or tool result it reads, and invokes tools that have their own vulnerabilities a malicious input can exploit into remote code execution. In every case the damage is bounded by what the execution environment holds and can reach. Sandboxing is how you make that boundary small. You're not trying to guarantee the agent never misbehaves; you're guaranteeing that when it does, it lands somewhere it can't hurt you.
Is a Docker container enough to sandbox an AI agent?
It depends on how much you trust the code, and for genuinely untrusted agent code the honest answer is usually no. A container is a real improvement over running code in your own process — namespaces, cgroups, dropped capabilities, and seccomp shrink the blast radius. But it shares the host kernel, so a kernel local-privilege-escalation or a container escape crosses the boundary, and mounted secrets or an inherited environment still leak. For semi-trusted code you've reasoned about, a hardened container can be fine. For model-generated, prompt-injectable code with access to real tools and data — the 2026 default — a microVM gives a stronger boundary (a separate guest kernel under hardware virtualization) that matches the 'assume the code is hostile' assumption you should be making.
Won't creating a fresh VM for every agent step make my agent slow?
It used to, which is exactly why snapshot-restore exists. The concern is real because the sandbox sits inside your agent's inner loop, so you create one constantly — a three-second cold boot per step would wreck the experience. Modern microVM platforms don't cold-boot per create; they restore a snapshot of an already-booted machine. On PandaStack the restore step is around 49ms, an end-to-end create is p50 179ms (p99 about 203ms), and a true cold boot happens only on the first spawn of a template (around 3s). That's hardware isolation at roughly container-grade latency. For stateful agents you can also use one VM per session or fork a warmed snapshot (same-host fork 400–750ms) so you're not even paying the create per step.
How do I keep the agent's sandbox from leaking my credentials?
Two rules that most stacks get wrong. First, don't put ambient credentials in the environment at all — no cloud role, no mounted secrets, no keys in the env. The environment starts with nothing, so there's nothing for the code to read. When a run genuinely needs access, mint a per-run credential scoped to exactly the resource it needs, valid for minutes, injected as a file a specific process reads, and block the guest's route to the cloud metadata endpoint (which is the most common escalation path). Second, deny egress by default and allowlist only the endpoints the run needs, so code that reads something still can't send it anywhere. Strong isolation with wide-open credentials and egress is the single most common 2026 mistake — the boundary contains the code but not the damage.
How do I sandbox a stateful agent that needs to keep files or a server running across steps?
You don't have to trade isolation for continuity — two patterns cover it. Give each agent session its own long-lived microVM for the duration of the task so state (working directory, installed deps, a running dev server) persists across steps, and tear it down when the session ends; the hard rule is one session per VM, never reused across users or tasks, and still no long-lived baked-in credentials. Or snapshot a VM that already has the toolchain and repo warmed and fork it per task — a same-host fork lands in 400–750ms with copy-on-write memory and disk, so each run gets a private, disposable machine that starts from a known-good state and still dies with its credentials when done. The second pattern also gives you best-of-N cheaply: fork the same warm state into several isolated attempts.
49ms p50 cold start. Fork, snapshot, and scale to zero.