An autonomous coding agent does something most software never does on purpose: it runs code nobody wrote by hand and nobody read before execution. It generates a script, runs it, reads the output, generates the next one. Point it at a repository and a shell and it will install packages, spawn subprocesses, reach the network and rewrite files, in a loop and often unattended.
That changes the question you ask of your sandbox. It is no longer "how do I keep my build out of your build?" It is "what happens when the code inside the box is trying to get out — because it was prompt-injected, because a dependency was poisoned, or because the model reasoned its way into something destructive?"
For that question, docker sandboxing — putting the agent in a container — is the default answer. This post is about where that boundary holds, where it stops, and what a microVM with its own kernel changes.
How container isolation works (and where it stops)
First, a name collision worth clearing up. Since early 2026, Docker Sandboxes is also a product: Docker's own tooling for running coding agents, driven by the sbx CLI. Its documentation is explicit about the boundary it draws — it runs agents in isolated microVM sandboxes, and each sandbox gets its own Docker daemon, filesystem and network. That settles part of the argument below before it starts: the company that did the most to make containers mainstream did not put an unattended coding agent in a plain container either. Whichever tooling you pick, the question underneath is unchanged — where does the isolation boundary sit, and what does it cost you when it is only kernel-deep?
Colloquially, though, "docker sandboxing" still means what most teams actually do: docker run the agent and move on. So here is what that gives you.
A container is not a small machine. It is a process — or a group of processes — running directly on the host's Linux kernel, with a set of kernel features arranged so that the process believes it has a machine to itself. Three primitives do the work:
- Namespaces give the process its own view of the world: its own PID tree, mount table, network stack, user and group IDs, hostname.
docker runsets these up so your process seesPID 1and a clean/. - cgroups cap what it can consume — CPU, memory, PIDs, block I/O — so one container cannot starve the others.
- Capabilities, seccomp and LSMs (AppArmor, SELinux) narrow what it can ask the kernel to do: drop
CAP_SYS_ADMIN, filter the syscall table down to a couple of hundred allowed calls, apply a mandatory-access-control profile.
Here is the part that matters: every one of those is a feature of the one shared host kernel. Container and host run the same kernel image. When code inside calls open(), socket() or ioctl() — any of the roughly 350 syscalls Linux exposes — the host kernel serves that call directly. The isolation is filters and views layered on top of a kernel the untrusted code is talking to.
That is an efficient design and a genuinely useful one. It is also a large shared surface. You can shrink it: gVisor puts a user-space kernel in front of the syscalls, a tight seccomp profile narrows what stays reachable, rootless containers reduce the blast radius. All three are mitigations for the same structural fact — untrusted code and your host are separated by kernel features, not by a kernel.
What a microVM changes
A microVM inverts the model. Instead of one kernel serving many isolated process groups, each sandbox gets its own kernel, memory and virtual devices, and the boundary between it and the host is enforced by the CPU's hardware virtualisation (Intel VT-x, AMD-V), mediated by KVM in the host kernel. If the model is new to you, what a microVM is covers it separately.
The practical difference is the interface the untrusted code talks to. Inside a microVM, when the agent's code calls open() or socket(), that syscall is served by the guest's own kernel, in its own address space. It never reaches the host kernel's syscall table. To affect the host, code would have to escape its guest kernel and then break out through the virtualisation boundary — a far narrower and far more scrutinised interface than all of Linux.
Firecracker is what makes this cheap enough to do per session. It is a minimal virtual machine monitor, written in Rust and open-sourced by AWS, that runs Lambda and Fargate. Its design is an argument for less surface, laid out in the original paper, Firecracker: Lightweight Virtualization for Serverless Applications (NSDI '20):
- It emulates only a handful of devices — a virtio block device, virtio-net, a serial console, a one-button keyboard for shutdown. No BIOS, no PCI, no USB, no legacy emulation, where a general-purpose hypervisor carries a broad catalogue of device models and the code that comes with it: the paper counts more than 1.4 million lines of code in QEMU 4.2.
- It boots a stripped Linux guest in milliseconds. The authors measured 146 ms at the 99th percentile for a pre-configured Firecracker microVM — one vCPU, 256 MB of memory, a minimal kernel and root filesystem, timed from forking the VMM process to the guest kernel forking its init, 500 samples on AWS hardware. Each microVM runs as its own process, itself confined by a seccomp jailer on the host side.
- The same paper measures around 3 MB of memory overhead per Firecracker microVM against around 131 MB for QEMU — the price of the device model Firecracker leaves out — which is what makes one microVM per workload a realistic unit of isolation rather than a luxury.
So the trade the industry lived with for years — virtual machines isolate strongly but cost too much, containers are light but weakly isolated — is largely what Firecracker dissolves. That is what the docker vs microvm question comes down to now. Not security against speed, but where you put the boundary. The same argument from the other direction: microVM vs container isolation.
Threat model: what a compromised agent can reach
Assume the realistic worst case: a prompt-injected or otherwise hostile agent with full code execution inside the sandbox. Where does it get?
| Attack vector | Container (shared kernel) | microVM (own kernel + KVM) |
|---|---|---|
| Reach the host kernel's syscall surface | Yes — served by the host kernel directly | No — served by the guest kernel; the host sees only the KVM and virtio interface |
| Escape path to the host | One runtime or kernel bug reachable through an allowed syscall | A guest-kernel escape and a break of the VM boundary — two independent barriers |
| Blast radius of a single kernel flaw | Every container on the host shares the affected kernel | Contained to one guest; the host kernel is not the workload's kernel |
| Cross-tenant reach | Mediated by namespaces and cgroups; weakened by any escape | Separate address spaces, memory and virtual devices per VM |
| Exfiltration over the network | Set by egress policy, not by the isolation model | The same — isolation is not egress control |
Most write-ups stop at "the kernel is shared" as a half-sentence. The consequence is the interesting part, and it is documented. CVE-2024-21626, disclosed in January 2024 as part of the Leaky Vessels set and scored 8.6 HIGH on CVSS 3.1, used a leaked file descriptor in runc to give a container process a working directory in the host filesystem — a full breakout, reachable by running a malicious image or building from a malicious Dockerfile. Five years earlier, in February 2019, CVE-2019-5736 broke out of runc too, by overwriting the host's runc binary from inside a container and taking root on the host with it — the same 8.6. Both were runtime flaws rather than kernel flaws, which is the point: a shared-kernel model has more than one place to go wrong, and the kernel adds its own history through subsystems such as io_uring, netfilter and waitid. A flaw of that class does not land on one tenant. It lands on every workload sharing that kernel at once.
Two things belong next to the table. First, prompt injection is the delivery mechanism nobody models. The attacker needs no foothold on your host to start; a poisoned README, an issue comment or a dependency's post-install script is enough to steer the agent, and the agent already has execution. Isolation does not prevent that step. It decides how far it travels.
Second, isolation is the easier half. A microVM stops the agent reaching the host and its neighbours. It does nothing about the likelier attack: the agent quietly sending your source, your .env or a stolen token to an endpoint you allowed. That is exfiltration, and egress control solves it. And microVMs are not magic — KVM and the monitor are code, and code has bugs. The claim is a smaller, more defensible surface plus a second independent barrier, not "unbreakable".
When a container is fine — and when you need a microVM
Containers earned their place, and most workloads do not need anything else. The dividing line is not how sensitive the data is. It is whether you can decide, in advance, that the next thing the workload does is safe.
| Situation | Boundary that fits | Why |
|---|---|---|
| Your own code in your own CI | Container | You wrote it and reviewed it. The threat model is hygiene between builds, not defence against the workload. |
| First-party services in production | Container | Kubernetes, service meshes, the whole cloud-native stack are built this way, and rightly so. |
| Trusted single-tenant dev environments | Container | One team, one codebase, no third-party execution. |
| Third-party or model-generated code you cannot review | microVM | You cannot verify in advance that the next action is safe — the definition of an agent executing generated code. |
| Unattended agent sessions | microVM | Nobody is watching the step where a prompt injection turns into a command. |
| Multi-tenant execution of untrusted workloads | microVM | One tenant's compromise must not become everyone's. |
| Regulated data | microVM | "Separated by kernel features on a shared kernel" is a hard boundary to defend to an auditor. |
Most estates contain both rows, and they do not have to be resolved into one answer. The cheap version of this decision is to keep the container where the workload is yours and move only the untrusted execution behind a harder boundary — the agent's session, not your build farm. What does not work is treating one row as if it were the other: hardening a container until it approximates a hypervisor means owning a syscall policy per workload, and the research that automates that job exists because doing it by hand does not hold — the Confine authors (RAID '20) note that policies derived from observing a running workload never exhaustively capture the code a future workload or a rare runtime condition will need. And it still leaves the shared kernel.
The honest framing: containers isolate cooperating workloads well. microVMs isolate adversarial ones. Agents running generated code are adversarial by default — not because the agent is malicious, but because you cannot verify its next action before it takes it.
How we do it at ainclave
ainclave is a managed sandbox for autonomous coding agents, built for the adversarial case. Every agent session boots its own Firecracker microVM with its own guest kernel behind the KVM line, so a guest-kernel bug is contained instead of shared. Because isolation is only half the problem, egress is deny-default: a session starts with no route out and reaches only the destinations its configuration names at create time — the repository host, the model endpoint, any configured MCP endpoint. That list is fixed for the life of the session and enforced on the host datapath, outside the guest. Filesystem policy follows the same rule: paths are declared at create time and cannot be widened from inside.
We are equally explicit about the edges. Our own seccomp filter is a targeted set of blocks, not a full allowlist. Proxy-bound credentials — the model key you bring, source-control tokens, tokens for remote MCP endpoints — reach the agent process only as placeholders; the environment variable inside the guest carries a reference, and the real bearer token is substituted on the outbound request, so only the destination server sees it. What we do not claim is a process boundary inside the guest: the proxy terminates TLS there, so whether the agent could reach that plaintext from the proxy process is untested. Physical single-tenancy exists in the dedicated tier, not on shared capacity. Those limits, and the roadmap against them, are written out where the boundary is enforced.
Conclusion
The container-versus-microVM choice is not about performance or fashion. It comes down to one question: does the code inside the box share a kernel with the host? For code you trust, sharing is fine and efficient. For code an agent generated seconds ago and is running unattended, the shared kernel is a large surface with a documented breakout history, and the isolation is filters on top of it rather than a wall.
A microVM gives that code its own kernel and a hardware-enforced boundary between it and your machine. Firecracker makes that cheap enough to do per session. Pair it with deny-default egress and both halves are covered — the escape and the exfiltration.
Where that boundary belongs in your own estate is a decision we would rather work through with a team than settle in the abstract. That is what the design partner programme is for: a small number of teams, telling us what they need to run agents on their code.