There is a specific moment in every agent architecture review where the room goes quiet. It is the moment someone asks where the code the agent just wrote gets executed, and the answer turns out to be "in the same container as the orchestrator." The agent that reads inbound emails, browses documentation pages, and summarizes support tickets is generating Python on the fly and running it next to the service account credentials, the database connection string, and the deploy keys.
Agents that write and run code are now the default, not the exotic case. Every major framework ships a code execution tool, every provider ships a code interpreter, and coding agents run shell commands hundreds of times per session. What did not become standard is the isolation underneath. Teams inherited whatever their framework defaulted to, and those defaults range from a proper microVM to subprocess.run() with a comment that says # TODO: sandbox this.
This post is a decision framework for that layer: what the isolation options are, how their escape surfaces differ, and why the configuration around the runtime matters more than the runtime itself.
Start From the Right Threat Model: The Code Is Attacker-Authored
The mistake is modeling agent-generated code as "code my trusted model wrote," which sounds like a mild insider risk. The correct model is "code an attacker wrote," because of how injection composes with tool use.
An agent with a code execution tool takes untrusted input constantly: web pages it browses, files it reads, API responses, retrieved documents, user messages. Any of those can carry instructions that steer the model into writing code the attacker wants run. We covered the injection mechanics in our piece on prompt injection in enterprise agents; the short version is that no prompt-level defense reliably prevents it. Once you accept that, the code execution tool is an arbitrary-code-execution primitive exposed to anyone who can influence the agent's inputs. That is roughly the definition of a remote code execution vulnerability, except you shipped it on purpose.
So the security question stops being "will the model write bad code" and becomes "when hostile code runs, what can it touch." That framing does two useful things. It makes the sandbox the load-bearing control instead of the prompt. And it gives you a concrete metric for comparing options: not vibes about safety, but what a process inside the boundary can read, execute, and connect to.
The Isolation Spectrum: Four Rungs, Very Different Escape Surfaces
Isolation options for agent code cluster into four tiers, and the differences come down to what sits between the untrusted code and your host kernel.
| Tier | Mechanism | Kernel exposure | Startup cost | Honest assessment |
|---|---|---|---|---|
| Shared process / subprocess | None, or language-level restrictions | Full host kernel, same privileges as the agent | Zero | Not a security boundary. Fine for demos only. |
| Container (runc) | Namespaces, cgroups, seccomp, capabilities | Shared host kernel via filtered syscalls | ~100ms - 1s | Defensible with hardening for low-risk tools; one kernel bug from escape |
| gVisor (runsc) | Userspace kernel intercepts syscalls | Sentry reimplements the syscall surface; host sees a narrow interface | Sub-second | Strong isolation, some syscall compatibility and I/O performance cost |
| MicroVM (Firecracker, Kata) | Hardware virtualization, separate guest kernel | Host kernel only via a minimal VMM | ~125ms - a few seconds | Strongest practical boundary; more operational machinery |
The container tier deserves the most scrutiny because it is where most teams land by default. A standard container is a process with namespace-scoped views of the system, talking to the same kernel as everything else on the host. Fly.io's engineering team put it bluntly in their sandboxing and workload isolation writeup: containers are a packaging and scheduling mechanism that you can harden into a security boundary, not a boundary by construction. The Linux kernel syscall interface is enormous, and container escapes historically arrive through it - dirty pipe, runc file descriptor leaks, io_uring bugs.
gVisor changes the shape of that exposure. Its architecture puts a userspace kernel called the Sentry between the application and the host: the sandboxed code's syscalls are intercepted and served by the Sentry, which itself talks to the host through a much smaller, heavily filtered interface. An exploit now needs to compromise the Sentry and then escape its restricted host access - two independent layers. The cost is compatibility (not every syscall is implemented) and overhead on syscall-heavy workloads. Google runs App Engine and Cloud Run atop it, which says something about the tradeoff being production-viable.
MicroVMs go one further and give untrusted code its own guest kernel. Firecracker's design is instructive for how minimal the attack surface gets: a VMM of roughly 50k lines of Rust, five emulated devices, no BIOS, no PCI, boot to userspace in about 125ms with around 5MiB of VMM overhead per instance. The NSDI 2020 paper describes why AWS built it for Lambda: thousands of mutually untrusting workloads per host, with hardware virtualization as the boundary between them. That is precisely the agent code execution problem, which is why the current generation of agent sandbox providers - Modal, E2B, Beam, Cloudflare, Blaxel - build on microVMs or gVisor rather than plain containers. Modal's survey of code execution sandboxes for tool-calling agents is a reasonable map of that provider space if you are buying rather than building.
One practical note on statefulness, since multi-step agent workflows need it: the microVM tier handles this better than its reputation suggests. Snapshot and resume lets a provider pause a VM mid-session for pennies and restore it with installed packages and interpreter state intact. You do not have to trade isolation strength for session persistence anymore, so do not let "we need stateful execution" push you down the spectrum.
Blast Radius Beats Runtime Brand
Here is the part that gets lost when the conversation fixates on Firecracker versus gVisor versus Docker: the runtime determines how hard it is to escape the box. The configuration determines whether the attacker needs to escape at all.
Consider two setups. Setup A is a Firecracker microVM with default networking, an instance metadata route, and the agent's API keys passed in as environment variables because that was convenient. Setup B is a boring runc container with no network interface, a read-only root filesystem, a tmpfs scratch directory, and zero credentials inside. Setup A has the stronger isolation technology and the larger blast radius. An injected payload in Setup A does not need a VM escape; it curls the metadata endpoint or reads the environment and exfiltrates over its open egress. The same payload in Setup B can burn CPU and write to scratch, and that is the whole incident.
So audit these three dimensions before you argue about runtimes:
- Filesystem scope. What is mounted, and writable, inside the boundary? Repo checkouts are common and often fine. The host's home directory,
.ssh,.aws, cloud config files, or the Docker socket are not. A mounted Docker socket converts any sandbox into root on the host, and it shows up in agent setups constantly because someone wanted the agent to build images. - Egress policy. Default-deny outbound, then allowlist per tool. This is the single control that breaks the exfiltration leg of the injection kill chain. PyPI and a package registry proxy for a Python tool; nothing at all for a data transformation tool. If the sandbox can POST to arbitrary domains, every secret and file inside it should be considered publishable.
- Credential reachability. The best number of secrets inside a code execution sandbox is zero. When a tool needs authenticated calls, put a broker outside the sandbox that holds the credential and exposes a narrow, audited operation to the code inside. This is the same least-privilege logic we laid out for restricting agent tool permissions, applied one layer down. Watch for indirect reachability too: cloud metadata endpoints, kubelet APIs, and internal service meshes are credentials with extra steps.
The CNCF cloud native security whitepaper frames workload isolation the same way - as a composition of runtime, network policy, and secrets handling rather than a single technology pick - and it is a useful reference when you need to justify this framing to a platform team.
Where Inherited Defaults Leak
Most teams did not choose their isolation layer; a framework or a coding agent chose it for them. A few recurring leak patterns from setups we have reviewed (details generalized; these are composite examples):
Command filters standing in for sandboxes. Coding agents often ship an allowlist of "safe" shell commands instead of an execution boundary. Allowlists over a shell are bypass museums: git alone can execute arbitrary code through hooks and core.pager config, and researchers keep cataloging fresh routes through symlinks and worktree tricks in exactly these filters. A command filter is UX for the human in the loop. It is not a boundary against injected instructions.
The orchestrator and the executor sharing a trust domain. The agent loop, which holds API keys and long-lived credentials, runs in the same container as the code it executes. This is the default in a lot of self-hosted framework deployments because splitting them takes work. It means every tool call executes with the orchestrator's full reach.
Session bleed in pooled sandboxes. Warm sandbox pools cut latency, and then one tenant's session leaves packages, cached files, or environment residue for the next. Isolation per session matters as much as isolation per host - a sandbox that is airtight against the kernel but shared across users leaks sideways instead of down.
Dev-container drift. The sandbox config that got a security review was strict. Six months of "the agent needs npm, open egress to registry.npmjs.org" then "it needs GitHub" then "just give it internet, we will tighten later" produced the current config, which nobody re-reviewed. Egress allowlists rot in one direction.
If your coding agents run on developer laptops rather than server-side sandboxes, the problem shifts shape but not severity - we wrote up that variant in runtime security controls for coding agents.
A Per-Tool Decision Record, Not a Platform Decision
The framing that makes this tractable: isolation is a property of a tool, not of your agent platform. A calculator tool, a SQL-read tool, and a shell tool have wildly different blast radii and should not inherit one sandbox config because they happen to live in the same agent.
For each tool that executes code, write down five lines:
- Input trust. Can untrusted content influence what code runs here? (For anything downstream of web browsing, email, retrieval, or user input: yes.)
- Runtime tier. Shared process, container, gVisor, microVM - and why. Untrusted-influenced code gets gVisor or a microVM. Hardened containers are for tools whose inputs you control end to end.
- Filesystem scope. Exact mounts, which are writable, lifetime of scratch space.
- Egress. Default-deny plus the explicit allowlist, or "none."
- Credentials reachable. Ideally "none"; otherwise the broker pattern and the specific operation exposed.
This takes about twenty minutes per tool and it changes two conversations. During design review, it surfaces the tool that quietly needs open egress and a secret at the same time - the combination that should trigger a redesign, per Simon Willison's lethal trifecta framing. During incident review, it is the difference between "we believe the sandbox contained it" and demonstrating what the payload could and could not reach. Auditors accept the second one.
The counter-intuitive consequence of doing this exercise honestly: some tools get to be less isolated than your instinct says. A templated SQL tool with parameterized queries against a read replica may not need a sandbox at all; it needs query validation. Spending your microVM budget where input trust is lowest, instead of wrapping everything uniformly, is what keeps latency and cost sane.
Getting the Boundary Built Without Stalling the Roadmap
This is the kind of work that fails as a ticket labeled "harden agent sandbox" because it cuts across the agent code, the network layer, and the secrets infrastructure. When OpenNash builds production agents, the per-tool isolation record is a design-phase deliverable, before any deployment: every code-executing tool gets its runtime tier, mounts, egress allowlist, and credential path documented and reviewed alongside the guardrails and human-approval points. The build then implements that record rather than a framework default, and the handoff includes it, so the client's security team can audit and evolve the boundary without archaeology. Ownership of the sandbox config matters as much as ownership of the agent code; inherited defaults are how the drift starts.
If you already have agents running code in production, the audit order is fixed by payoff: egress first, credentials second, runtime tier third. Cutting open egress and pulling secrets out of the execution environment are usually days of work and they neutralize the exfiltration path even on a weak runtime. Migrating from containers to gVisor or Firecracker is a bigger lift, and it can proceed calmly once the first two are done.
Pull up your highest-traffic agent this week and answer one question from inside its code execution environment: run env, list the filesystem, and try an outbound request to a domain you control. Whatever comes back is your current blast radius, and it was chosen for you. The rest of this framework is how you choose it on purpose.