Skip to content
← Journal
7 min readAta Mohammadi

Hardening Agent Sandboxes: MicroVMs, gVisor and WebAssembly for Untrusted Code

An AI agent running shell commands is an arbitrary code execution primitive you installed on purpose. Here is the isolation ladder — container, gVisor, Firecracker, WebAssembly — what each one actually stops, and what it costs per sandbox.

Here is a sentence that should worry you more than it usually does: "the agent runs the build and fixes the failures."

Running the build means executing package.json lifecycle scripts, which means executing arbitrary code from a dependency tree you did not audit, chosen by a model that can be talked into things by a README. Fixing the failures means the agent writes new code and runs that. You have built a remote code execution endpoint and pointed it at your laptop, or worse, at CI where the credentials live.

The industry answer in 2026 is unambiguous, and it is worth stating plainly: docker run is not a security boundary for AI-generated code. The question is what you use instead, and the honest answer is "it depends on the threat model" — so let's make the threat model explicit first.

What are you actually defending against?

Four distinct threats, which different tools address:

  1. Accidental destruction. rm -rf, a force-push, a migration against the wrong database. No malice, just an agent with more reach than judgement. This is by far the most common and the cheapest to fix.
  2. Prompt injection to exfiltration. A file in the repository, a package README, an issue comment or a fetched web page contains instructions. The agent reads them as input and follows them, posting your .env to a pastebin. Network egress is the attack surface here, not the filesystem.
  3. Supply-chain execution. A postinstall script in a transitive dependency mines, scans or exfiltrates. This predates agents entirely; agents just install more packages, faster, with less review.
  4. Deliberate escape. Code specifically written to break out of the sandbox and reach the host kernel. Rarest, most expensive to stop, and the only one that actually requires a hypervisor.

Most teams start by defending against (4), which is the hardest, and skip (2), which is the likeliest. Egress control is the highest-value control per hour of work in this whole article.

The isolation ladder

Container (runc) gVisor (runsc) microVM (Firecracker / Kata) WebAssembly
Kernel Shared with host Shared, behind a user-space kernel Dedicated per sandbox None — no syscalls at all
Boundary Namespaces, cgroups, seccomp Syscall interception in Sentry Hardware virtualisation (KVM) Sandboxed VM with capability-based imports
Escape surface Whole host kernel syscall API ~50 vetted host syscalls Hypervisor + virtio devices Host functions you explicitly grant
Cold start ~50–200 ms ~200–500 ms ~125 ms boot, 5–30 ms from snapshot Sub-millisecond
Per-sandbox overhead Tens of MB Tens of MB, plus syscall latency ~5 MB of VMM plus the guest Hundreds of KB
Runs arbitrary binaries Yes Yes, with some syscall gaps Yes No — needs a Wasm-targeted build
Honest verdict Trusted code only Good default when KVM is unavailable The production answer Ideal when you control the workload

Two rows deserve elaboration.

gVisor puts a user-space kernel (Sentry) between the workload and the host. An application syscall is trapped and serviced inside Sentry, which itself only makes a small, vetted set of host syscalls. You trade a large attack surface for a small one, and you pay for it in syscall latency — which is why gVisor is excellent for I/O-light workloads and poor for anything that hammers the filesystem. A npm install is roughly the worst case.

Firecracker is a minimal VMM: each sandbox gets its own Linux kernel under KVM, with almost no device emulation. The compelling property for agent work is not the boot time but snapshot restore — you can pause a microVM with its filesystem and memory intact and resume it in tens of milliseconds. Boot the toolchain once, snapshot, and every subsequent agent task starts from a warm, identical, disposable environment. That turns "sandboxes are too slow to use per task" into "sandboxes are faster than the alternative", which is the whole ballgame.

Kata Containers is worth knowing as the pragmatic packaging of the same idea: OCI-compatible, so it slots in as a runtimeClass in Kubernetes without rewriting anything.

Where WebAssembly fits

WebAssembly is the strangest and most interesting option, because its security model is different in kind rather than in degree. A Wasm module has no syscalls. It cannot open a file, resolve a hostname or spawn a process unless the host explicitly passes it a function that does. The default is not "restricted access" — it is no access.

That is capability-based security, and it is the model you would design if you were starting today.

The state of play as of late 2026:

  • WASI 0.2 (the component model release) is the stable, widely supported baseline.
  • WASI 0.3 shipped in June 2026, moving async into the component model itself with native async func, stream<T> and future<T>, replacing the wasi:io poll-based machinery. Wasmtime 43+ and jco support it.
  • WASI 1.0, the production-stable LTS milestone, is targeted for late 2026 or early 2027.

The limitation is the one in the table: the workload must be compiled to Wasm. That makes it an excellent fit for evaluating model-generated functions — a data transform, a scoring rule, a policy expression, a plugin — and a poor fit for "run the repository's test suite", which needs a real Node or Python and a real filesystem.

A reasonable architecture uses both: Wasm for the hot path of evaluating small generated snippets, a microVM for the occasional full build.

Building the thing

Start with the sandbox that runs today

Before any of the above, the sandbox nearest to hand is usually the one your agent runtime already offers — most now default to filesystem and network restrictions scoped to the working directory. Turn it on. It handles threat (1) entirely, for free.

Deny egress by default

This is the control that matters most and gets implemented least.

default: DENY all outbound
allow:   registry.npmjs.org, pypi.org        (package installs)
allow:   github.com, objects.githubusercontent.com  (source)
allow:   <your model provider>
deny:    everything else, including DNS to unlisted names

Implement it as a filtering proxy the sandbox must route through, not as iptables rules inside the sandbox — a rule the workload can edit is not a rule. Log every denied request: a denied request to an unknown host is the highest-signal alert in the entire system, because there is no benign reason for a build to reach one.

While you are there: do not mount credentials into the sandbox at all. No ~/.aws, no ~/.ssh, no .npmrc with a token, no GITHUB_TOKEN in the environment. If the agent needs to push a branch, it hands the diff to a host-side process that does the push. The credential never enters the blast radius. Prompt injection cannot exfiltrate a secret that is not there.

Give each task a fresh, disposable root

# Snapshot a warmed microVM once, per toolchain image
firecracker --api-sock /tmp/fc.sock \
  --config-file vm-config.json
# …boot, install toolchain, then:
# PATCH /snapshot/create  { "snapshot_type": "Full", ... }

# Per task: restore, run, discard. Nothing persists between tasks.
# PUT /snapshot/load  { "resume_vm": true, ... }

The property you are buying is not just isolation — it is identity. Every task starts from a byte-identical environment, so a failure is reproducible and "it worked on the previous run" stops being a category of bug.

Constrain resources, and mean it

An agent in a retry loop will happily consume every core you give it.

Limit Why
CPU quota A runaway loop should be slow, not fatal to the host
Memory cap OOM-kill the sandbox, never the supervisor
Wall-clock timeout Hard kill; an agent has no concept of "too long"
Disk quota A generated log file will fill a disk given the chance
Process limit Fork bombs are still a thing

Do not let the sandbox decide it is finished

This is the point where sandboxing meets orchestration. The verification gates from the previous article in this series — type check, lint, tests, diff boundary — must run outside the sandbox, or at minimum their results must be transported out and evaluated by something the workload cannot influence. A test suite that reports its own success, inside an environment the agent controls, reports nothing.

The uncomfortable part

Every control above is defence in depth against a system you deliberately gave a shell. None of them make prompt injection safe; they make it survivable. The agent will still be manipulable by content it reads, because reading content and following instructions are the same operation for a language model, and no amount of kernel isolation changes that.

So the design principle is not "make the agent trustworthy". It is:

Assume the agent is fully compromised by whatever it last read. Now decide what it is allowed to reach.

That framing produces different answers than a threat model built around escapes. It puts egress filtering above hypervisors, credential absence above credential scoping, and a human reviewing a diff above any amount of automated verification.

Run agents in microVMs. Snapshot them so it is cheap. Deny egress by default. And keep the secrets on the other side of the wall.


Sources: Northflank — How to sandbox AI agents in 2026, WASI 0.3 release notes, WASI roadmap

Next step

Tell us what is broken or what should exist.

Send the shape of the problem and any constraints you already know — budget, deadline, the stack you are stuck with. You will get a written reply from the engineer who would do the work, not a sales sequence.