# Sandboxing code an agent wrote for you

Once an agent writes and runs code, you are executing untrusted input on your infrastructure. The isolation question has known answers. The harder problems are quotas, cancellation and getting results out.

By Elson Tan (https://elsontan.com)
Published: 2026-05-19
Reading time: 4 min
Tags: Agents, Security, Architecture
Canonical: https://elsontan.com/blog/sandboxing-agent-written-code/

> Code an LLM generates is untrusted input and needs process-level isolation, not a try/catch. Give each session its own container, run the coding loop inside it rather than in your request handler, and design for the operational problems: per-tenant quotas, mid-run cancellation, and streaming partial output.

---
My agent platform has a skill where you describe something you want built and the agent writes the
code and runs it. Clean this spreadsheet. Scrape this list. It works well.

It also means a stranger can type a sentence and my servers will execute whatever program comes out
of it.

People overcomplicate the security question here. Nobody is worried the model has gone rogue. The
point is simpler: a user's prompt is input I do not control, the model turns that input into a
program, and I run it. It does not matter what the model intended.

So the code needs a real boundary around it. What I did not expect is that the boundary turned out
to be the easy part.

## Pick the boundary, then stop thinking about it

The options are well documented and the decision is not close for most teams.

| Boundary | Isolation | Start time | When it fits |
| --- | --- | --- | --- |
| Same process, `vm` or `exec` | None worth the name | Instant | Never, for untrusted code |
| Container, shared kernel | Namespaces and cgroups | Sub-second | Trusted-ish workloads |
| [gVisor](https://gvisor.dev/docs/) | Syscalls intercepted in user space | Sub-second | Untrusted code, most cases |
| [Firecracker](https://firecracker-microvm.github.io/) microVM | Hardware-enforced | Around a second | Untrusted code, hostile tenants |

Do not try to do this inside your language. Node's `vm` module tells you in its own documentation
that it is not a security boundary, and the history of JavaScript sandbox escapes is basically one
long list of people finding a way to reach back out to the host.

I use [Cloudflare's Sandbox](https://developers.cloudflare.com/sandbox/), which runs on
[Containers](https://developers.cloudflare.com/containers/), one per session. That choice was
mostly about not operating a second orchestration system, and the isolation properties come from
the platform rather than from anything I wrote. Which is the point. Whichever row you pick, pick it
once.

## The design decision that mattered more

I originally ran the coding loop in the Worker: the model proposed an edit, the Worker applied it,
ran a command, fed the output back. It kept hitting the model's output limit and truncating tool-call
JSON mid-string, which fails in a way that is tedious to diagnose because the error surfaces as
malformed arguments rather than as a length problem.

The version that works inverts it. The Worker does not run a coding loop. It boots a container, runs
a coding agent inside it, and streams that process's output back. Two models, two jobs:

- A cheap fast model in the orchestrator, talking to the user and deciding to delegate.
- A strong model driving the coding agent inside the container, iterating until the task is done.

The orchestrator never sees the file edits. It sees a task id, progress lines, and a result. Its
context stays small, and the coding loop is bounded by the container rather than by a single
response.

## What breaks in production

Isolation is table stakes. These are the problems that generate tickets.

Cancellation is the one to design for on day one. A user says stop while the container is mid
`npm install`, and you need a control path that does not depend on the running process cooperating.
That means killing the container and treating the partial workspace as garbage. Retrofitting this
into a loop that assumes it runs to completion is a rewrite, not a patch.

Quotas come next. One tenant should not consume the pool. Mine are enforced before the container
boots, against the same token wallet the rest of the platform uses, because container time and model
tokens both cost money and a tenant does not distinguish between them.

Then there is getting results out. The container has a filesystem the user cannot see, so I mirror
workspace changes to object storage as they happen. A run cancelled at 80% still leaves the files it
wrote. Streaming progress as newline-delimited JSON into a key the frontend polls was the least
clever option available and has never caused a problem.

Last, put the timeout on the task rather than the request. A coding task can legitimately take
minutes; the HTTP request that started it cannot. Anything long-running goes through a queued object
with its own alarm, and the request returns a task id immediately.

## Should the container reach the network?

Mostly no, and the exception is annoying.

Code that installs a dependency needs a package registry. Code that fetches a URL because the user
asked it to needs the open internet, which is the same SSRF exposure a webhook sender has, except
the attacker gets to write arbitrary code rather than supply a URL.

I allow package registries and block private address ranges. It is a compromise rather than a
solution, and I would tighten it to a proxy with an allow-list if the workload justified the work.
Anyone claiming a network-enabled sandbox is fully contained has not thought about DNS.

## Sources

- [Cloudflare Sandbox](https://developers.cloudflare.com/sandbox/), Cloudflare
- [Cloudflare Containers](https://developers.cloudflare.com/containers/), Cloudflare
- [gVisor documentation](https://gvisor.dev/docs/), Google
- [Firecracker microVM](https://firecracker-microvm.github.io/), Amazon Web Services
- [Server Side Request Forgery Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html), OWASP
