
Isolation
Sandboxing Agents: What Each Isolation Boundary Actually Contains
Process sandboxes, containers, gVisor, microVMs, VMs and Wasm each stop something different. A boundary-by-boundary guide for agents that run code, drive browsers, or control a desktop.
TL;DR
Your sandbox probably doesn't need to stop an escape. It needs to stop a request going out. If the box has outbound network access and a credential file mounted inside it, the data leaves through the front door while everyone is still arguing about kernel escape.
An agent that can curl an arbitrary host has defeated your microVM without touching it.
So we'd put the design effort somewhere other than where the debate usually happens. Not container versus gVisor versus Firecracker. What credentials live inside the box, where it's allowed to send packets, what filesystem it can see, how long it stays alive, and how much authority its session carries.
Pick the strongest boundary your latency budget can afford, then go and close the egress. That's the one that decides how your incident ends.
Overview
A container is an excellent packaging and resource-management tool and a mediocre one for containing hostile code. That distinction is worth sitting with, because containers are what most teams reach for and the reasons they are good at the first job have almost nothing to do with the second.
The pattern is consistent: teams pick an isolation boundary based on what is convenient or what the platform provides, without first naming what they are trying to contain. The result stops a threat they did not have and permits the one they did.
So: the boundary options, and what each one actually stops as against what it quietly lets through. Then three agent workloads that need containment most and have almost nothing in common, being code execution, browser automation and computer use. Then the operational details that decide whether any of it works.
To keep it concrete, picture one team with three agents, and imagine them as a retail operations group. Agent one writes Python and runs it against a warehouse extract to answer ad-hoc merchandising questions. Agent two logs into supplier portals with real credentials and downloads invoices, because half the suppliers have no API. Agent three drives a Windows desktop client for a legacy ERP that has no API at all, using screenshots and synthetic clicks. Same team, three containment problems with almost nothing in common. They are made up, and they are the three shapes that keep coming up.
Start from the exfiltration goal, not the escape goal. For almost every agent, what someone wants is to get data out, or to cause an action in a system the agent can reach. Not a root shell on your host. Design the boundary that stops that, and the escape hardening becomes defense in depth rather than the whole plan.
Name the Threat Before Choosing the Box
There are four meaningfully different threat models behind the phrase "sandbox the agent," and they call for different boundaries.
Accident containment. The agent, behaving as designed, does something destructive by mistake. Replit's agent deleting a production database during a declared code freeze in July 2025 is the canonical case: no attacker, no exploit, just an agent with more reach than the situation warranted and a "freeze" the platform had no mechanism to enforce. The boundary that helps here is scope and credentials, not virtualisation.
Untrusted-code containment. The agent generates code and the code is hostile, because the prompt that produced it was attacker-influenced, or because it pulled in a malicious dependency. This is the case people mean when they compare gVisor and Firecracker, and it is the case where kernel-level isolation genuinely matters.
Confused-deputy containment. The agent holds legitimate authority and is manipulated into using it on the attacker's behalf. Norm Hardy named this failure mode in The Confused Deputy (1988); it is the dominant threat for browser and desktop agents, and the one isolation is worst at, because nothing escapes. Every action is authorised. This needs authorisation-layer and human-process controls, not a stronger box.
Multi-tenant containment. Several customers' agents share infrastructure and must not reach each other. Hardware-backed isolation and per-tenant network segmentation, non-negotiable.
Most agent deployments face the first and third of these, and buy tooling for the second. That mismatch is the recurring error.
The Boundary Options, Honestly
Process and language-level sandboxes. Restricted interpreters, seccomp-filtered subprocesses, RestrictedPython, JavaScript isolates. These stop accidents and casual misuse cheaply and with no cold-start cost. They do not stop determined code: language-level restriction has a long history of escape via reflection, and any bug in the sandbox implementation is a full escape. Use them for expression evaluation and deterministic transforms on trusted inputs, never as the only boundary around model-generated code.
Containers. The thing to internalise is that a container is a namespace and resource boundary with some security properties, not a security boundary that also does packaging. NIST SP 800-190 (September 2017, and still the reference) puts it carefully: containers "provide a strong degree of isolation" but "do not offer as clear and concrete of a security boundary as a VM," because they share the same kernel and can be run with varying capabilities and privileges, so "the degree of segmentation between them is far less than that provided to VMs by a hypervisor." NIST's failure mode is a "carelessly configured" environment rather than a categorical one, but the kernel is the shared attack surface either way, and it is large.
The escape history is not theoretical. CVE-2019-5736 in runc (CVSS 8.6) let a container process overwrite the host runc binary via /proc/self/exe. CVE-2024-21626, part of the 2024 "Leaky Vessels" set (also CVSS 8.6), used a leaked file descriptor to give a container access to the host filesystem namespace. Both were in the runtime everyone uses.
A container can nonetheless be hardened substantially:
- Seccomp. Docker's default profile is an allowlist that "disables around 44 system calls out of 300+," and the documentation explicitly recommends against changing it. A workload-specific profile is much tighter still.
- User namespaces. Remap container root to an unprivileged host UID, so an escape lands as nobody rather than as root.
- Dropped capabilities.
--cap-drop=ALL, adding back only what is genuinely required, which for a Python interpreter is usually nothing at all. - Read-only root filesystem plus
no-new-privileges, with a small writabletmpfsfor scratch. - No host mounts, no Docker socket. A mounted
/var/run/docker.sockis a root shell on the host with extra typing.
A container with all of these is reasonable for code unlikely to be hostile. It is still one kernel bug from the host.
Sandboxed runtimes: gVisor and Kata. These attack the shared-kernel problem from opposite directions. gVisor interposes what its own docs call an application kernel running in user space, the Sentry, which implements the Linux System API itself, so no system call is passed through directly to the host; the Sentry then talks to the host through a minimal, explicitly enumerated syscall surface plus a Gofer process for filesystem access. Its documentation is candid about the limits: "in general, gVisor does not provide protection against hardware side channels"; for exploits in execution paths outside an intended System API it defers to the host and the platform, where there is "little defense that gVisor can provide"; and, the line worth quoting in a design review, "a sandbox is not a substitute for a secure architecture." The costs are syscall-heavy overhead and imperfect compatibility. Kata Containers goes the other way, running each pod inside a lightweight VM with its own guest kernel, using hardware virtualisation as the isolation basis while keeping OCI-compatible workflows. An escape now has to cross a virtualisation boundary rather than only namespace and cgroup enforcement, but Kata's own threat model is precise about where that boundary is thin, enumerating virtio device backends, vsock, the virtiofsd filesystem daemon, VFIO passthrough and ACPI as attack surfaces, and noting that vhost-net and vhost_vsock run inside the host kernel, where "an exploit would be already running within the kernel space." On that path there is no hypervisor left to defeat.
MicroVMs. Firecracker is the reference point. The project documents a startup time under 125 ms and memory overhead under 5 MiB per microVM, achieved with an aggressively minimal device model, six emulated devices in the current repo docs: virtio-net, virtio-block, virtio-vsock, virtio-balloon, a serial console, and "a minimal keyboard controller used only to stop the microVM." It adds a jailer that drops privilege and then exec()s into the VMM as an unprivileged process, and per-thread seccomp filters restricting the host syscalls Firecracker itself may use; the design docs cite five microVMs per host core per second, about 180 per second on a 36-core host, for a microVM with a minimal kernel, a single core and 128 MiB of RAM. This is the sweet spot for per-task isolation of genuinely untrusted code: VM-grade separation at container-ish cost.
Full VMs. Slower, heavier, and the right answer when the agent needs a full desktop, GPU passthrough, or a Windows guest. Also when a compliance reviewer wants a boundary they recognise without a lecture.
WebAssembly. A different shape of guarantee. The Wasm specification provides bounds-checked linear memory, call stacks the application cannot reach, type-checked control flow, and no undefined behaviour, so the design leaves a module no direct path to host memory. WASI then grants system access capability-by-capability: a module opens only the directories it was explicitly given, and Wasmtime adds a guard region ahead of each linear memory and post-instance zeroing, done, in its own words, "in the face of other bugs," which is the reason not to say "by construction." The limits are equally explicit in Wasmtime's security documentation: side channels get only "a few forms of basic spectre mitigations," and everything depends on the capabilities you granted. Wasmtime does treat execution-time denial of service, memory exhaustion, uninterruptible loops, as a security bug rather than an accepted limitation; only compile-time DoS is out of scope. Wasm is excellent for embedding untrusted plugins with a small auditable capability set, and awkward for "run whatever Python the model wrote," because the ecosystem the model expects mostly is not there.
Figure 1 — Nested isolation boundaries and what an attacker must defeat at each level. The network path on the right requires defeating none of them.
The Forgotten Boundaries
These four decide the actual outcome far more often than the choice between gVisor and Firecracker.
Network egress. Default-open egress makes every other boundary decorative. If exfiltration is the goal, and it usually is, the attacker needs one outbound request, not a kernel exploit. Deny by default, allowlist per tool and per task, force traffic through a proxy that logs, and remember that DNS is a data channel: a resolver that answers arbitrary queries is an exfiltration path even when HTTP is blocked.
Allowlists also rot. ForcedLeak, the Salesforce Agentforce vulnerability disclosed by Noma Security in September 2025 and rated CVSS 9.4, exfiltrated CRM data to a domain that was on Salesforce's trusted-URL allowlist but had expired and become available for purchase, the researchers bought it for five dollars. An allowlist is a live inventory that needs ownership, not a configuration you write once.
Filesystem scope. The sandbox should see the task's inputs and nothing else. No home directory, no ~/.aws, no ~/.ssh, no repository checkout the task does not need, no shared volume that other tasks also mount. Prefer copying inputs in and results out over mounting a live path.
Secret availability inside the box. This is the single most consequential decision, and it is orthogonal to the isolation technology. If the sandbox holds a long-lived token, then any code that runs inside it inherits the agent's full authority and the boundary bought you almost nothing. The alternative: the sandbox can request an action through a broker that injects credentials at call time on the outbound leg, so the raw secret never exists inside the sandbox. An attacker who fully owns the sandbox then owns only the actions the broker will perform, under whatever policy the broker enforces.
Resource and time limits. CPU, memory, disk, wall-clock, and a cap on process count. This is availability and cost control more than security, but an agent in a retry loop against a metered API is a real incident, and an unbounded sandbox is a cryptomining host waiting for a prompt injection.
Case 1: Code Execution
The merchandising analysis agent writes Python and runs it against a warehouse extract. The threat model: the code is model-generated, so its content is influenced by whatever entered the context window, including customer-supplied product descriptions and supplier emails that the analysis draws on. Treat the code as untrusted by default, because its provenance is untrusted by default.
A defensible configuration:
sandbox:
boundary: microvm # or gVisor if cold start dominates
lifetime: per_task # destroyed after each run
filesystem:
rootfs: read_only
writable: [/tmp] # tmpfs, 512 MiB
inputs: copy_in # parquet extract, no live mount
network:
egress: deny
allow: [] # nothing; results come back over the control channel
credentials:
mode: none # no tokens in the box
limits:
cpu: 2
memory_mb: 4096
wall_clock_s: 300
max_processes: 64The two decisions doing the real work are egress: deny and credentials: none. With those, hostile generated code can corrupt its own results and waste five minutes of CPU. It cannot reach the warehouse, cannot phone home, and cannot persist. The choice of microVM versus gVisor then becomes a latency and cost question rather than a security one, which is the position you want to be in.
The case where this gets hard is when the analysis genuinely needs live data. The answer is a narrow, audited data proxy: the sandbox may issue parameterised queries against an allowlisted set of views through a single permitted endpoint, and the proxy, not the sandbox, holds the warehouse credential and enforces row limits. That is a tool boundary, not an isolation boundary, and it belongs in the design either way.
Case 2: Browser Automation
The supplier-portal agent is a harder problem than code execution, and most teams underestimate it because a browser feels like a familiar, already-sandboxed thing.
It is not the browser process that is the problem. It is the session. The agent operates inside an authenticated context with real supplier-portal credentials, and every page it visits is untrusted content that reaches the model. Brave's research through 2025 documented this repeatedly across agentic browsers: in Perplexity's Comet, page content was passed to the LLM without distinguishing user instruction from page text, letting invisible payloads, white-on-white text, HTML comments, drive cross-site actions including reading one-time passcodes from mail. A follow-up disclosure in October 2025 showed the same class of attack delivered through screenshots, where faint text, light blue on yellow, was recovered by the browser's text recognition and executed as instruction, and extended the findings to Fellou and Opera Neon as well.
Brave's summary of the structural failure, in that second post, is the sentence to take away: agentic browser assistants can be prompt-injected by untrusted webpage content, "rendering protections such as the same-origin policy irrelevant because the assistant executes with the user's authenticated privileges." The web's isolation model assumes a script confined to its origin. An agent is not confined to an origin; it is a user with hands.
Figure 2 — The browser-agent confused-deputy problem. The attacker supplies intent; the agent supplies authority; the boundary that failed is authorisation, not isolation.
What actually helps, roughly in order of effect:
- One session per task, never a shared profile. A fresh, ephemeral browser profile holding exactly the credentials for the one site the task needs. The supplier-portal agent should not have a profile that is also logged into mail.
- Egress allowlisted to the target origin. The agent may talk to
supplier-portal.exampleand nothing else. This is the control that turns a successful injection into a nuisance. - Run logged out where you can. OpenAI ships a logged-out mode in ChatGPT Atlas for exactly this reason and recommends it when the task does not need authenticated access. Most read-only browsing does not.
- Site-scoped grants and confirmation on consequential actions. Anthropic's Claude for Chrome pilot uses site-level permissions the user grants and revokes, asks before high-risk actions like publishing, purchasing, or sharing personal data, and blocks high-risk categories such as financial services, adult content and pirated content. Their red-teaming numbers, all measured in autonomous mode, are the argument for why structural controls are necessary rather than optional: across 123 test cases and 29 attack scenarios, 23.6% attack success without mitigations and 11.2% with them, with a challenge set of four browser-specific attacks reduced from 35.7% to zero.
- Put the browser in a VM anyway. Not because escape is the main threat, but because a disposable VM gives you a clean profile, an enforceable network path, and a resettable snapshot, three things that are awkward to guarantee on a developer's machine.
The residual risk cannot be isolated away. If the task requires an authenticated session on the supplier portal, an injected page can at worst do what a legitimate user of that portal could do. Bound that by scoping the portal account itself: a read-only invoice-download role, not the account that can change remittance details.
Case 3: Computer Use
The legacy-ERP agent sees a full desktop through screenshots and acts through synthetic mouse and keyboard events. This is the hardest of the three to constrain, for a reason that is worth stating precisely: an agent that can see the screen and click anywhere defeats most in-application permission models. Those models assume a human is on the other side of the dialog. A permission prompt is a UI element, and the agent can click "Allow."
Screenshots also widen the injection surface in a way that is easy to miss. Any text visible anywhere on that desktop is model input. A notification toast, a rendered email, a spreadsheet cell, a filename. Brave's screenshot findings apply directly: instructions can be embedded at contrast levels a human will not notice and text recognition will happily recover.
Anthropic's own guidance for its computer use tool is the clearest published statement of the containment posture, and it is worth following literally: use a dedicated virtual machine or container with minimal privileges; avoid giving the model access to sensitive data such as login information; limit internet access to an allowlist of domains; and ask a human to confirm decisions with meaningful real-world consequences, including things as mundane as accepting terms of service. The documentation also states that "in some circumstances" the model "will follow commands found in content even when they conflict with your instructions," including instructions in webpages and images, and that a classifier layer runs on screenshots to steer the model toward asking for confirmation when injection is suspected, while noting that this extra protection "won't be ideal for every use case (for example, use cases without a human in the loop)."
Translated into a build for the ERP agent:
- A dedicated, disposable VM per task, built from a golden image, with the ERP client and nothing else installed. No mail client, no browser signed into anything, no password manager, no chat.
- A scoped ERP service account whose permissions match the task. Post goods receipts, don't create vendors. The desktop boundary cannot constrain what the account can do, so the account must be the constraint.
- Egress restricted to the ERP host. A desktop agent with open internet is the widest exfiltration channel of the three, because it can read anything on screen and type anywhere.
- Human confirmation at the irreversible steps, with the screenshot and the field diff shown rather than the agent's description of what it did.
- Full session recording. Which brings us to the operational layer.
Lifecycle and Audit
A boundary that is not reset is not a boundary for long.
Per-task versus per-agent lifetime. Per-task is the correct default: a fresh sandbox for each unit of work, destroyed afterwards, so nothing planted in run n is present in run n+1. Long-lived per-agent sandboxes are cheaper and warmer, and they turn any single compromise into persistence. If you must keep one warm, reset it from a snapshot rather than cleaning it in place, cleaning in place is a trust decision about code you no longer trust.
Snapshot and restore is what makes per-task affordable. Restoring a microVM from a memory snapshot skips the guest boot entirely, which is how sub-second cold starts are reached in practice. Two cautions. A restored snapshot resumes with the wall clock frozen at snapshot time and with identifiers, cached random numbers and cryptographic tokens replicated across every restore, Firecracker's own documentation says that absent a mechanism guaranteeing unique things stay unique, it considers "resuming execution from the same state more than once insecure," and the VMGenID reseeding available on newer guest kernels covers only the kernel entropy pool, not the rest. And a snapshot taken after the sandbox saw untrusted content is a snapshot of a possibly-tainted machine, snapshot from the golden image, never from a used sandbox.
Cost and cold start are the real constraints. Rough ordering, worth measuring on your own hardware rather than trusting a table: a warm process sandbox starts in single-digit milliseconds, a container in tens to hundreds, gVisor somewhat more than a container, a Firecracker microVM around a hundred milliseconds cold and far less from snapshot, a full VM in seconds. If an interactive agent makes forty tool calls per conversation, a 400 ms per-call sandbox is not viable, scope the sandbox to the session and reset between sessions.
Audit means capturing what happened inside the box, not just at its edge. The valuable record is: the exact code or actions executed, every outbound connection attempted including denied ones, every file written, the resulting artefacts, and a hash of the input set. For desktop and browser agents add video or a screenshot timeline; a text log of a click at (412, 806) is not evidence of anything. Denied egress attempts deserve particular attention, the highest-signal event a sandbox produces, because legitimate workloads rarely generate them and a compromised one generates them immediately.
The reset is the control. An isolation boundary's value decays with every task that reuses it. Ephemeral by default, snapshot-restored from a clean image, and never carrying state forward that untrusted content could have touched.
Matching Boundary to Blast Radius
Choose the boundary from the threat model, then check what still leaks.
| Workload | Recommended boundary | What still leaks |
|---|---|---|
| Deterministic transforms on trusted inputs | In-process restricted interpreter, resource-capped | A bug in the interpreter is a full escape; nothing else contains it |
| Model-generated code in a developer loop, no production reach | Hardened container: rootless, cap-drop=ALL, workload seccomp profile, read-only rootfs, no host mounts | Kernel privilege escalation; anything reachable on the network; any credential you mounted |
| Model-generated code where the prompt is attacker-influenced | gVisor or Kata, per task, egress deny | Hardware side channels; data you copied in; whatever the allowlist permits |
| Genuinely untrusted or multi-tenant code | Firecracker microVM or full VM, one per task, snapshot-restored | Side channels; rare guest-to-hypervisor escapes; egress |
| Plugin-style extension inside a host process | Wasm with a minimal WASI capability set | Side channels; runtime implementation bugs; exactly the capabilities you granted |
| Browser automation on authenticated sessions | Disposable VM, one ephemeral profile per task, egress allowlisted to the target origin | Everything that session is authorised to do, the confused deputy is not contained by the box |
| Full desktop / computer use | Dedicated disposable VM, golden image, scoped service account, egress to one host, session recording | Everything visible on that desktop; in-app permission dialogs the agent can click through |
Figure 3 — Choosing a boundary. Every path terminates at the same requirement, because egress is the boundary that determines the outcome.
Conclusion
The isolation question everyone debates, container versus gVisor versus microVM, is real but secondary. It decides what happens if hostile code tries to escape. What determines the outcome of an actual incident is: which credentials exist inside the box, where the box may send packets, what filesystem it can see, how long it lives, and whether the authority it wields is scoped to the task.
Choose the strongest boundary the latency and cost budget can carry, then spend most of the design effort on egress, secrets, and lifetime. For browser and desktop agents, accept early that isolation is not the primary control: those are confused-deputy problems where nothing escapes and every action is authorised, so the real work is narrowing the authority the session carries and putting a human at the irreversible steps.
The honest version of "we run it in a sandbox" names the boundary, the threat it stops, and the threat it does not. A team that can say "per-task Firecracker microVM, egress denied except the data proxy, no credentials in the guest, destroyed after each run, residual risk is whatever the proxy will do on request" has a sandbox. A team that says "it's in Docker" has a deployment target.
What each box contains is a design output, not a product feature. Write down the escape path at every level and the exfiltration path that needs no escape at all. If the second one is shorter than the first, the boundary you chose is not the boundary you needed.
Sources
- NIST SP 800-190, Application Container Security Guide
- Norm Hardy, "The Confused Deputy (or why capabilities might have been invented)", ACM SIGOPS OSR, 1988
- gVisor — Security Model (architecture guide)
- Kata Containers — Threat Model
- Firecracker — Design documentation (device model, jailer, seccomp)
- Firecracker project site — startup time and memory footprint claims
- Firecracker — Snapshotting support, including snapshot security and uniqueness
- Wasmtime — Security
- Wasmtime — What is considered a security vulnerability
- Understanding prompt injections: a frontier security challenge — OpenAI, 7 Nov 2025 (ChatGPT Atlas logged-out mode)
- Docker — Seccomp security profiles for Docker
- Red Hat Security Bulletin RHSB-2024-001, Leaky Vessels — runc (CVE-2024-21626)
- Agentic browser security: indirect prompt injection in Perplexity Comet (Brave)
- Unseeable prompt injections in screenshots: more vulnerabilities in Comet and other AI browsers (Brave)
- Piloting Claude for Chrome — safeguards and red-teaming results (Anthropic)
- Computer use tool — security considerations (Claude platform documentation)
- ForcedLeak: AI agent risks exposed in Salesforce Agentforce (Noma Security)
- Vibe coding service Replit deleted user's production database (The Register, 21 Jul 2025)