Coding agents now run arbitrary, LLM-generated code by default — npm install, test suites, shell one-liners — which turns "sandbox untrusted code" from an occasional need into a load-bearing part of the architecture. Here's what actually stands between an agent's code and your host kernel.
A coding agent that can only read and suggest is a toy. The useful ones run things: they execute the test suite, run a shell command to check a file exists, install a package to see if the API surface matches what they assumed, or execute a Python snippet to sanity-check a calculation before writing it into a PR. That's the entire value proposition — an agent that has to stop and ask a human to run npm test isn't an agent, it's a text generator with extra latency.
The problem is that "run arbitrary code produced by a language model, on every task, at scale" is a security posture that didn't really exist five years ago in this form. It's not quite the classic "sandbox untrusted user uploads" problem (the code isn't adversarial in the traditional sense — nobody is trying to pwn you), and it's not quite the classic CI/CD problem either (CI runs code a human wrote and reviewed before it landed on main). It's closer to running code from a moderately competent stranger, thousands of times a day, where occasionally that stranger's code does something genuinely dangerous by accident — deletes the wrong directory, runs pip install against a typosquatted package, opens a socket somewhere it shouldn't, or (increasingly) gets steered by content it reads mid-task, a prompt-injection payload buried in a file or web page it fetched. The containment question is the same either way: what happens when the code inside the sandbox tries to get out?
Most people's mental model here is "put it in a Docker container." That's a reasonable first instinct and a genuinely wrong security boundary, and understanding why is the key to understanding what production agent sandboxes actually do differently.
A container is a process. That sentence is doing all the work. When you docker run something, you get a process (or a small tree of them) that shares the host's kernel with every other process on that machine. Namespaces (pid, net, mnt, user, ...) change what that process can see — its own PID 1, its own filesystem root, its own network stack. Cgroups change what it can consume — CPU shares, memory limits, I/O throttling. Neither of those mechanisms was designed as a security boundary in the sense of "an attacker inside this cannot reach the host." They were designed for resource multiplexing and namespacing, and security-relevant isolation is a side effect, not the design goal.
The attack surface that matters is the syscall interface. Every container still makes raw syscalls directly into the host kernel — read, write, mmap, ptrace, mount, hundreds of others, each one a slice of C code inside the kernel that has to correctly police every corner of arbitrary input. The Linux kernel is enormous and none of it was written with "an untrusted tenant is calling this" as its primary threat model, because for most of its history it wasn't. When that assumption breaks, you get a container escape: CVE-2019-5736 let a malicious container overwrite the host's runc binary via a /proc/self/exe symlink trick and get code execution on the host the next time anything invoked runc — including an unrelated, honest container starting up. That's not a hypothetical; it shipped, it was exploitable, and it's the textbook example of why "it's containerized" is not a security argument on its own.
seccomp profiles (syscall allowlists/denylists) and AppArmor/SELinux policies narrow this, and a well-configured container with a tight seccomp profile and dropped capabilities is meaningfully safer than a default one. But you're still trusting that the specific set of syscalls you did allow has no exploitable bug in the specific kernel version you're running, for every tenant, forever. That's a bet you're making on kernel code you didn't write, exposed directly to code you don't trust.
There are two structurally different ways to actually fix this, and the AI code-execution sandbox market has converged on both.
gVisor, built by Google and open-sourced in 2019, inserts an application kernel — Google calls it the Sentry — written in Go, between the untrusted process and the real Linux kernel. The sandboxed process still thinks it's talking to Linux; it still calls open() and socket() like normal. But those calls are intercepted (via ptrace or, faster, via KVM as a syscall-trapping platform) and handled by the Sentry's own reimplementation of the syscall, which then makes a much smaller, much more constrained set of calls to the actual host kernel to do the real work.
The security argument is straightforward: the Sentry's reimplementation of read or mmap is orders of magnitude smaller and newer than the actual Linux kernel's, so there's simply less attack surface to find a bug in, and the surface that remains is a codebase built specifically with "this input is hostile" as the first assumption. You're not trusting the whole Linux kernel's syscall table anymore — you're trusting the Sentry, plus the much smaller slice of the host kernel the Sentry itself is allowed to touch. This is the same isolation model behind Google Cloud Run and GKE Sandbox.
The cost is performance: intercepting every syscall through a user-space kernel adds real overhead, especially for syscall-heavy workloads (lots of small file I/O, for instance). For a coding agent running pytest or npm install, that overhead is usually tolerable; for something doing tight I/O loops, it can be a real tax.
Firecracker, built by AWS and open-sourced in 2018, takes the other approach: instead of shrinking what an untrusted process can reach inside a shared kernel, give every tenant an entire kernel of its own. It's a minimal virtual machine monitor built on KVM — each sandboxed workload gets its own microVM, with its own kernel, its own memory space, isolated by hardware virtualization rather than by software policy on a shared kernel. The "micro" part matters: Firecracker strips the device model down to a handful of virtio devices and nothing else, which is why it can boot a functioning microVM in on the order of 100ms rather than the seconds a traditional VM takes.
This is the isolation model behind AWS Lambda and Fargate, and it's also the model E2B (a widely used sandboxing platform built specifically for AI-agent code execution) is built on: each code-execution session runs in its own Firecracker microVM, so a coding agent's python exploit.py moment is contained by the same hardware-boundary-of-a-VM that's supposed to hold up even against a kernel-level compromise inside it, not by a syscall filter that has to get every rule right.
The cost here is different: you're running a real (if minimal) kernel per sandbox, which means more baseline memory overhead per tenant than a shared-kernel container, and you need KVM available on the host — which rules out running this nested inside another VM without nested virtualization support, a real constraint for some cloud environments.
Here's the trap: gVisor and Firecracker both answer "can the code inside the sandbox break out to the host or other tenants?" Neither one, by itself, answers "can the code inside the sandbox do damage while staying inside its own boundary?" — and for an AI agent specifically, that second question matters just as much.
An agent's code doesn't need a kernel exploit to cause a real problem. It needs network egress to exfiltrate a .env file it found lying around, or to a package index it can pip install a typosquatted dependency from, or to a webhook a prompt-injection payload told it to call. It needs no isolation bug at all to burn your API budget in an infinite retry loop, or to rm -rf a mounted volume it was given legitimate write access to because that access was scoped too broadly to begin with. Both gVisor and Firecracker will faithfully contain all of that inside the sandbox they promised to contain it in — which is exactly the problem, because the damage was never a containment failure. It was a privilege the sandbox correctly granted.
That's why production agent sandboxes layer default-deny network egress (allowlist specific domains rather than routing everything), short, hard session TTLs instead of long-lived sandboxes, ephemeral filesystems that discard state between runs instead of persistent mounts, and resource quotas independent of the isolation technology itself. The isolation primitive — gVisor or Firecracker — is necessary but answers a narrower question than "is this safe to run," and treating it as the whole answer is how teams end up surprised.
If you're building or evaluating a system that lets an LLM execute code — your own agent, or a third-party sandbox provider you're integrating — the question worth asking isn't "is it containerized," it's "what's the actual isolation primitive underneath, and what happens to network and filesystem access when the sandbox is behaving exactly as intended." A Firecracker microVM that lets the agent make outbound HTTPS to anywhere is more exposed in practice than a well-configured Docker container with a locked-down egress policy, even though the former has the stronger kernel boundary. Isolation technology and access policy are two separate decisions, and most of the incidents that actually happen live in the second one.