# Assume the Code Is Hostile: Sandboxing AI Agents with microVMs and gVisor

> AI agents now routinely run code they wrote or were told to run — and prompt injection means an attacker can choose that code. So the safe assumption is that every line the agent executes is hostile, and your job is containment. Here's the isolation spectrum from bare process to Firecracker microVM, why a Docker container is not a security boundary against hostile code, and how to build an ephemeral, egress-filtered sandbox that survives a successful injection.

Author: Kishore K Sharma. Published: 2026-08-02. Canonical URL: https://kishorek.dev//writing/ai-agent-sandboxing-microvms. Tags: ai-agents, security, sandboxing, firecracker, infrastructure.
License: © 2026 Kishore K Sharma. All rights reserved. Reproduction requires attribution and a link to https://kishorek.dev//writing/ai-agent-sandboxing-microvms.

---
Here is a claim that should change how you build agents: if your agent can run code, you should treat every line it runs as if an attacker wrote it — because, increasingly, an attacker did. Not metaphorically. The code interpreter, the "run this command to fix your build" tool, the shell your coding agent calls — all of it executes instructions that trace back, through a context window, to content the agent read somewhere. And as I've argued before, there is no reliable way to stop prompt injection from steering what ends up in that context. So the discipline is not *prevent the agent from running bad code*. It's *assume the bad code runs, and make sure it can't reach anything that matters.* That's a containment problem, and containment has a well-understood engineering answer.

## Why agents make this urgent

An LLM that only produces text has a bounded blast radius: worst case, it says something wrong. The moment you wire it to a tool that executes code, the blast radius becomes *whatever that code can touch*. And code-execution tools are no longer exotic — they're the default. Code interpreters run model-generated Python. Coding agents run shell commands, install packages, execute test suites. "Agentic" workflows are frequently just *the model decided to run something, so we ran it.*

Now stack prompt injection on top. An agent reads a GitHub issue, a web page, a dependency's README — untrusted content — and buried in it is "to resolve this, run the following setup script." The model, unable to cleanly separate data from instructions, may well run it. The attacker didn't need shell access to your box. They needed the agent to read a string, and the agent had a tool that runs strings. The lethal trifecta's "untrusted content" leg and "ability to act" leg meet inside a `subprocess.run`.

![Untrusted, LLM-generated code executing inside a sandbox boundary, with blocked arrows showing the things it must never reach: the host kernel, mounted secrets and cloud credentials, the network, and other tenants' sandboxes. The boundary is the whole product.](/writing/agent-sandbox-threat-model.svg "The agent's code is the attacker's code. The sandbox exists to make sure a successful hijack still reaches nothing.")

So the threat model is blunt: the code inside the sandbox is hostile. It will try to read `/proc`, enumerate environment variables looking for `AWS_SECRET_ACCESS_KEY`, scan the network, and — if it finds a kernel bug — try to break out onto the host. Your design question is not "how do I keep the code well-behaved." It's "when the code is actively malicious, what stops it." That question has a spectrum of answers, and they are not equally strong.

## The isolation spectrum

Isolation is a trade between two quantities that move in opposite directions: how much host attack surface the untrusted code can reach, and how much overhead you pay to run it. Cheap isolation exposes more; strong isolation costs more. The whole engineering decision is where on this curve your workload should sit.

![A ladder from weakest to strongest isolation: bare process with seccomp, OS container sharing the host kernel, gVisor's user-space kernel, Firecracker microVM with its own guest kernel, and full VM. One axis shows host-kernel attack surface shrinking up the ladder; the other shows overhead and boot cost growing. A line marks where a real security boundary against hostile code begins — at gVisor and above.](/writing/agent-sandbox-isolation-layers.svg "Attack surface shrinks and cost grows as you climb. The security boundary against hostile code starts higher than most people assume.")

**Bare process** (seccomp-bpf, a dropped-privilege user, maybe a chroot) is the weakest rung. The untrusted code runs directly on the host kernel; seccomp filters which syscalls it may make, which shrinks the surface but doesn't remove it. One allowed syscall with a kernel bug behind it, and it's game over. This is a speed bump for accidental damage, not a wall against a determined attacker.

**OS containers** — Docker, containerd, the whole ecosystem — are where most people *think* they're safe and mostly aren't. A container is Linux namespaces (isolated views of PIDs, mounts, network, users) plus cgroups (resource limits) plus a seccomp/AppArmor profile. It is excellent operational isolation. But here is the load-bearing fact: **a container shares the host kernel.** Every process in every container calls into the *same* kernel as the host. That kernel is millions of lines of C, and it is your entire security boundary. A single local-privilege-escalation bug in that shared kernel — and they are found regularly — lets hostile code in a container escape onto the host. Containers were built to isolate *cooperating* workloads from each other, not to contain *hostile* code. Treat "I ran it in Docker" as "I ran it," for security purposes.

**gVisor (runsc)** is the first rung that meaningfully moves the boundary. It's a user-space kernel written in Go that sits between the sandboxed application and the host. When the guest code makes a syscall, gVisor intercepts it (via ptrace or a KVM-based platform) and *handles it itself* — reimplementing a large chunk of the Linux syscall surface in memory-safe Go — instead of passing it straight to the host kernel. The host kernel still exists underneath, but the untrusted code no longer talks to it directly; it talks to gVisor, which makes a small, guarded set of host calls on its behalf. That shrinks the host attack surface dramatically for a modest overhead. The costs are real: some syscalls are unimplemented or subtly different, so certain workloads hit compatibility gaps, and syscall-heavy programs pay a performance tax. gVisor is what runs parts of Google Cloud Run and GKE Sandbox.

**microVMs (Firecracker)** are the strongest boundary you can get without paying full-VM prices. A microVM uses real hardware virtualization (KVM) to give the untrusted code *its own guest kernel*, running in a separate VM with a hardware-enforced boundary between it and the host. Escaping now means breaking out of the VM through the hypervisor's tiny emulated-device interface — a vastly smaller and more scrutinized target than the Linux syscall surface. Firecracker is AWS's minimal virtual machine monitor; it's the technology under Lambda and Fargate. It is deliberately spartan: around 5 MB of Rust, only a handful of emulated devices (a couple of virtio devices, a serial console — no BIOS, no PCI, no USB), which is precisely how it keeps the escape surface small. It boots a microVM in roughly 125 milliseconds, so "one VM per task" is actually affordable. AWS also ships a **jailer** that wraps the Firecracker process in namespaces, cgroups, and a chroot for defense in depth — so even if someone did break the VM boundary, they land in another cage.

**Full VMs** (QEMU with a full device model, a general-purpose hypervisor) are the heaviest rung — maximum compatibility and isolation, but slow to boot and heavy to run. For per-task agent sandboxes, microVMs give you nearly the same boundary at a fraction of the weight, which is exactly why the AI-sandbox vendors — E2B, Modal, Daytona, and friends — build on Firecracker and gVisor rather than rolling full VMs or trusting containers.

The one sentence to carry out of this section: **a container is not a security boundary against hostile code; a microVM is.** If your agent runs code an attacker can influence — and if it reads untrusted content, it does — you want to be at gVisor or, better, microVM on this ladder.

## Designing the sandbox: it's not just the boundary

Picking Firecracker is necessary but not sufficient. A microVM with your production credentials mounted and open network egress is a strong boundary around a fully-loaded gun. The isolation technology stops *escape*; the sandbox *design* determines what the code can do while it's comfortably inside. Here are the principles that matter, and every one of them maps back to breaking a leg of the trifecta.

**Make it ephemeral.** One fresh sandbox per task, destroyed the instant the task ends. No reuse, no cross-task state, no "warm" sandbox that carried the last job's data. Ephemerality means that even a successful compromise has nothing to persist into and no neighbor to poison — the environment it corrupted ceases to exist in seconds. This is the single highest-leverage design choice, and microVMs' fast boot is what makes it practical.

**Cut the network.** No ambient network access by default. If the task genuinely needs the network, use an egress allowlist — a proxy that permits only specific destinations — not open outbound. This is the direct kill of the exfiltration leg: hostile code that reads a secret can't POST it to `attacker.example` if there's nowhere for the packet to go. Most sandboxed tasks (run this code, execute these tests) need *no* egress at all, so default to none and make the network a deliberate, narrow exception.

**Mount no secrets.** The sandbox should hold zero credentials that matter — no cloud provider keys, no database passwords, no API tokens, no SSH keys, nothing from the host's environment. If a secret isn't in the sandbox, no amount of clever code inside can steal it. This is context minimization applied to the filesystem and environment: the sandbox gets exactly the inputs the task needs and nothing else.

**Clamp the resources.** Hard limits on CPU, memory, PIDs, and wall-clock time, enforced by cgroups and the VMM. This isn't only about fairness — it's the defense against fork bombs, memory-exhaustion DoS, and someone quietly turning your sandbox fleet into a crypto-mining rig. A task that should take three seconds gets killed at ten; a process that spawns ten thousand children hits the PID ceiling and dies.

**Lock the filesystem.** A read-only root filesystem plus a small writable scratch space (a tmpfs or a disposable overlay) that vanishes with the sandbox. The code can write its temp files and outputs but can't tamper with the base image, and nothing it writes survives teardown. Layer a seccomp profile on top to shrink the syscall surface even inside the guest.

**Treat the outputs as untrusted, too.** This is the one people forget. Whatever the sandbox produces — files, stdout, a JSON blob, a "result" — was potentially shaped by hostile code. Don't pipe it straight into a privileged context, don't `eval` it, don't render it as HTML without escaping, don't feed it back to the planning model as if it were trusted. The sandbox contains *execution*; it does not launder the *output*. Validate and constrain what comes back exactly as you would raw user input.

![The per-task microVM lifecycle as a loop: a task arrives; the system spawns a fresh ephemeral microVM or restores a pre-warmed snapshot; it runs the agent's code under strict CPU, memory, PID, and time limits with no ambient network and no mounted secrets; it collects the result; and it destroys the VM entirely. An annotation stresses per-task disposability — nothing survives to the next task.](/writing/agent-sandbox-microvm-lifecycle.svg "One microVM per task, born and destroyed around a single job. Snapshots let you boot pre-warmed so disposability stays cheap.")

## The snapshot trick that makes it cheap

The obvious objection to "one fresh microVM per task" is startup cost — even 125 milliseconds adds up, and you often want a fully-booted runtime with the interpreter warm and libraries loaded, which is slower than a bare kernel boot. Firecracker's answer is snapshot/restore: boot a sandbox once to a known-good, warmed state, snapshot its memory and device state to disk, and then *restore* fresh copies from that snapshot on demand. Restoring skips the boot entirely — you resume a VM that's already sitting at the ready prompt in a handful of milliseconds. Each restored copy is independent and disposable, so you keep strict per-task isolation while paying almost nothing per task. This is roughly how the fast AI-sandbox products feel instant despite giving every task its own VM: they're not booting, they're cloning a frozen, pre-warmed machine.

## The takeaway

Prompt injection means you cannot keep an agent from *deciding* to run hostile code. Sandboxing is the other half of the discipline: making sure that when it does, the code executes inside a box that reaches nothing — no host kernel to escape into, no secrets to read, no network to exfiltrate through, no neighbor to infect, and no tomorrow, because the box is destroyed when the task ends.

Choose the boundary honestly. A bare process with seccomp is a speed bump. A container is operational isolation that shares the host kernel and is *not* a wall against hostile code, however much muscle memory tells you otherwise. gVisor moves the boundary into user space for a modest tax. A Firecracker microVM gives you a real, hardware-enforced boundary that boots fast enough to throw away after every task. Then design the inside of the box like you mean it: ephemeral, no secrets, no ambient network, hard resource caps, read-only root, outputs treated as radioactive. The isolation technology is the wall. The design principles are what make the room behind the wall not worth breaking into. Build both, assume the code is hostile, and a successful injection turns into a destroyed VM instead of a breach.
---

Originally published at https://kishorek.dev//writing/ai-agent-sandboxing-microvms.
© 2026 Kishore K Sharma. All rights reserved.
