Sandbox matrix: what isolation do you actually get?
What a process, a rootless container, runc, gVisor, Firecracker, and Kata actually separate for an agent: filesystem, network, kernel, and privilege.
Why Sandbox? What's wrong with existing solutions
To get maximum productivity from an AI agent, we need to give it maximum autonomy, while also minimizing risk. A sandbox is a key component here.
An agent that can run a shell will run one - so once you start an agent, it can completely take over the machine it is running on if it wants to. Any stored credential on the machine becomes available to the agent, any file on the machine can be read and sent to the cloud. The only way to guarantee that an agent doesn't have access to these things is to make sure its execution environment does not have access to them. That's where a sandbox comes in.
What's wrong with normal VMs and containers? They isolate well, but they are not built for the way agents work: sharing the workspace as context, and keeping secrets out of the agent's reach. We will come back to those trade-offs below.
Overview - Ok, how do we sandbox?
We will cover the technologies we can use for sandboxes, the trade-offs for each, and finally review what projects in the industry use.
This is a map of the sandboxes people put around agents and other untrusted workloads: a plain process, a user namespace (including rootless containers), an OCI container on runc, gVisor, Firecracker, and Kata. The table is the default shape of each one. The notes are the configurations that delete a cell.
The matrix
| Layer | Filesystem | Network | Kernel | Host privilege |
|---|---|---|---|---|
| Shared kernel | ||||
| Process | Caller's files | Caller's network | Shared | Caller's uid and capabilities |
| Container | Own rootfs. Bind mounts are host files | Own network namespace, unless joined to the host | Shared | Uid 0 is host uid 0 |
| Rootless container | Own rootfs, as the mapped uid | Own network namespace, usually via slirp4netns or pasta | Shared. More of the kernel is reachable without host root | Root inside is an unprivileged host uid |
| Isolated kernel | ||||
| gVisor | Host paths only through the gofer | User-space netstack, unless host networking is selected | Sentry handles guest syscalls. The host sees a small syscall set | Sentry runs unprivileged |
| Separate kernel | ||||
| Firecracker | Guest disk image. No host-directory device | Guest NIC only if you attach one | Guest kernel, behind KVM and a small device model | Guest root stays in the guest |
| Kata | Guest rootfs. Shared volumes follow the VMM, often virtio-fs | Guest NIC, then the pod network | Guest kernel. Host surface is the VMM you configured | Guest root stays in the guest |
The trade-off runs down the rows: the easier it is to share files with the sandbox, the more of the host the sandbox can reach.
Shared kernel
Every row below runs on the host kernel. The differences are how much of it the workload can reach, and with what privileges. Within this level the real distinction is the user namespace: whether uid 0 inside the sandbox is uid 0 on the host.
Process
A new process gets its own address space and file-descriptor table. That is the isolation Unix has always had. It is not a sandbox.
It runs as the user who started it, with that user's capabilities and namespaces. A coding agent launched from your shell can open ~/.ssh, ~/.aws, and the kubeconfig, because you can. It can connect to anything you can, including services bound to localhost.
A process can be tightened until it resembles a sandbox - seccomp-bpf, Landlock, bubblewrap are a few examples. If the thing in front of you is a subprocess, this is the row you have.
The kernel is the host kernel. A local kernel bug is a host bug. Dropping privileges can mitigate some attacks, but local privilege escalations (LPEs) are easier to find when the kernel is shared.
Container
An OCI container - Docker, containerd, or Podman - is a group of Linux namespaces around a root filesystem, plus cgroups, plus the seccomp and LSM profile the runtime applied. runc is the program that usually enters those namespaces.
The mount namespace gives the process a new root; host paths are absent unless bind-mounted. The pid namespace hides the host's process list. On Docker's default bridge, localhost is the container's localhost (not the host). cgroups can be used to cap CPU, memory, and IO.
Docker's default daemon does not enable user namespaces. Uid 0 in the container is uid 0 on the host (That is container breakout with root in the container gets you root on the host).
Rootless container
A user namespace gives the process a new uid map. Inside it, a mapped uid can look like 0 and hold every capability in that namespace; on the host it is whatever the map says - often the calling user, or a subuid from /etc/subuid. This means that a container breakout with root in the container does NOT get you root on the host.
Side Note: Enabling user namespaces
The user namespace is the switch between the two rows above. In Kubernetes it is opt-in: the pod sets spec.hostUsers: false. With that, the container's uid 0 maps to an unprivileged host uid and the pod gets the rootless row. Without it, you get the default: uid 0 is host uid 0.
bubblewrap is the same idea applied to a single process instead of a container. It builds a user namespace, a mount namespace, and a pid namespace around one command, with no daemon and no rootfs. Codex and Claude Code use it on Linux to sandbox the commands an agent spawns. It is the process row, tightened.
Trade-offs
The shared kernel is where filesystem sharing is easy. A bind mount or a volume is just a path; the sandbox sees the same files the host does, no need to copy/rsync/scp/etc... That matters for agents, because the workspace is the context. Mounting the repo, the build cache, or the home directory lets your agent work at native speed without the need to copy everything in and out. Startup is fast and any Linux binary runs.
A shared kernel means that a kernel bug reachable from inside is a host bug, and uid 0 in a rootful container is uid 0 on the host. The user namespace removes the second problem but not the first. The seccomp profile shrinks the reachable syscalls, but it is an allowlist with a wide middle.
While not impossible, there's no easy way to control egress or do credential injection.
Snapshot and restore is not "easy" to do here, with various harnesses doing approximations of this (tracking code changes and reverting them when restoring a session checkpoint). A container's state is entangled with the host kernel - open files, sockets, in-flight syscalls - so checkpointing needs CRIU and is fragile. That is why the snapshot column in the table below is empty for the container-based projects.
Isolated kernel
gVisor implements the kernel in user space. It is not the host kernel, and it is not a separate kernel in a VM - it sits between the two.
gVisor
gVisor keeps the OCI interface and replaces the kernel the application talks to. Syscalls are intercepted and handled by the Sentry, a user-space kernel written in Go that runs unprivileged.
The default network is inside the Sentry, so the sandbox's localhost is not the host's. Host networking is a configuration option, and gVisor documents it as a reduction in isolation.
The gain is a much smaller set of host syscalls: the Sentry has to be compromised before a guest syscall becomes a host syscall. It is not a hardware VM - the Sentry is a reimplementation, so some things fail or behave differently. A bug in the Sentry lands in its own unprivileged process. That second hop is real, but it is a different boundary from a guest kernel.
Trade-offs
gVisor keeps most of the filesystem-sharing convenience of the shared kernel - the workspace mounts the same way - while cutting the host syscall surface to a small set. It needs no /dev/kvm, so it runs on CI runners and nested VMs where a microVM cannot.
The cost is compatibility and performance. The Sentry is a reimplementation, so some things fail or behave differently. It is also a large user-space program, and a bug in it is code execution as the user who started it - still unprivileged, but a real second hop.
Snapshot and restore is the payoff. The whole guest state lives in the Sentry's memory, so checkpoint/restore is native - that is what Substrate's sub-500ms resume is built on.
Separate kernel
The two rows below run a separate kernel. The host kernel sees a virtual machine monitor; the workload sees a guest kernel. This is the strongest isolation in the matrix: a guest kernel exploit stays in the guest, unless it can continue into KVM or the monitor's device model.
The guest filesystem is a disk image you attached; sharing a host directory is not part of the device model, so it needs a daemon on the host. The guest network is a NIC you attach - no NIC means no network. The VM boundary does not choose destinations.
Guest root is root of the guest. The monitor is a separate host process, meant to run unprivileged. A monitor running as root, with its API socket reachable by the workload you were trying to contain, is a control plane sitting outside this matrix.
VMs need /dev/kvm. A CI runner or nested VM without nested virtualization cannot start them.
Firecracker and Kata
The difference between the two is the layer they sit at. Firecracker is a VMM: a specific piece of software with a fixed, minimal device list - no USB, no arbitrary PCI passthrough, and no way to share a host filesystem. Kata is a container runtime that wraps a VMM: it keeps the OCI and pod interface, and the node picks the hypervisor. The name Kata in a manifest does not record which one the node used - the runtime configuration does. Kata's hypervisor choices are the thing to read before treating two Kata pods as the same sandbox.
The guest runs a small agent that sets the container up. It sits inside the VM.
Trade-offs
The separate kernel is the strongest boundary, and the hardest to share with. A block device for the rootfs keeps the host out of the guest's filesystem entirely; a workspace volume from the host brings a sharing daemon back. VMs also cost more than the other rows - a kernel to boot, memory for the guest - and they need /dev/kvm on the node.
Another advantage of VMs over containers is that they snapshot cleanly. The whole machine state is well-defined - memory, disk, devices - so a VM can be frozen and resumed as a unit.
One more note - The image is not the sandbox
The same rootfs under runc, gVisor, and Kata is three kernels; the image does not record which one ran. And the workspace is supposed to be visible - the question is every other path: the home directory, the Docker socket, kubelet credentials, and the host's /proc and /sys when those really are the host's.
What the projects use
The runtime's name is a weak source of truth, and so is the product's name. Here is where a few popular agent sandboxes land on the matrix.
| Project | Sandbox | Snapshot / restore | Matrix row |
|---|---|---|---|
| Agent Substrate | gVisor by default, or a Kata microVM on Cloud Hypervisor. Plain Linux containers are not supported; the microVM class needs /dev/kvm | Yes - checkpoint/restore. Idle actors are snapshotted out of memory and restored in under 500ms | Isolated, or separate |
| E2B | Firecracker. One microVM per sandbox, in its own cgroup and network namespace, with an nftables egress firewall | Yes - a sandbox is a resumed snapshot, not a booted kernel; pause and resume | Separate |
| Modal | gVisor or microVMs | Yes - memory, filesystem, and directory snapshots | Isolated, or separate |
| OpenHands | Docker. Each conversation runs in its own container, or directly on the host if you skip the sandbox | No | Shared (container), or process |
| Claude Code | Local: permission system plus sandboxed bash (bubblewrap on Linux, Seatbelt on macOS). Cloud: isolated VMs | No | Shared (process, tightened), or separate in the cloud |
| Codex | Local: bubblewrap on Linux, Seatbelt on macOS, the Windows sandbox on Windows. A workspace-write sandbox bounds files and network | No | Shared (process, tightened) |
| Daytona | OCI/Docker-compatible sandboxes. Marketing claims a dedicated kernel, but the public repo is archived and the runtime is hard to verify | Yes - stateful environment snapshots for persistent agent sessions | Shared (container), per its OCI/Docker basis |