Essay / Architecture · 2026-08-21

The environment is the sandbox

Sandboxing one shell command was the right idea at the wrong scale. A coding agent operates a repository, package manager, compiler, dev server, and browser. Leave one outside and the boundary has a door in it.

The claim. The unit of isolation for a coding agent is the complete development environment—not the model process, not the shell command, and not the Git checkout.
A developer starts one h5i box containing the agent, workspace, process controls, network gate, and resource limits
The fast path is still a complete boundary. The agent and every process it starts inherit the same filesystem, syscall, network, and resource policy.

Consider the apparently harmless task “upgrade the date library and fix the failing tests.” The agent edits one manifest and runs the package manager. The package manager resolves forty transitive dependencies. One of them executes a post-install script. The script reads the environment, probes the home directory, opens a socket, and exits successfully. The final diff contains a version bump and a lockfile. Nothing in those two files records the interesting part.

That is the scale mismatch. We tend to draw the risky object as the agent's shell command, while the work actually fans out into a temporary software supply chain. The command is only the first edge.

Command wrappers fit the world they were designed for. A program receives input, performs one bounded action, and returns output. You can put a wall around that moment.

A coding agent does not live in that world. It reads a repository, edits several files, invokes a package manager, starts a compiler, watches tests, launches a server, opens a browser, reads the console, and tries again. The work is a loop. Its children are part of the work.

If the agent is confined but its package scripts are not, the scripts own the machine. If the shell is confined but the browser uses your normal profile, the page inherits your sessions. If the repository is a worktree but the process still sees your home directory, checkout separation has been mistaken for authority separation.

Three boundaries that are too small

The model process

Watching only the agent executable assumes all consequential actions pass through its tool protocol. They do not. A build tool can spawn a compiler, which can invoke a linker, which can execute a helper. An install hook may run before the agent sees its next prompt. The process tree, not the first process, is the relevant object.

The command

Wrapping npm test helps only if every route to npm test uses the wrapper. An autonomous session makes hundreds of calls. Security that depends on the agent remembering the prefix is a convention, not a boundary.

The checkout

A Git worktree answers where edits land. It says nothing about ~/.ssh, cloud credentials, Unix sockets, the host network, or a browser profile. Git separates trees. It does not separate authority.

What belongs inside?

Put every component that can execute code or carry session state on the same side:

That turns a scattered list of dangerous operations into one object with a lifecycle: create, work, inspect, export, remove.

A boundary needs an output gate

Containment is incomplete if the agent can write directly back to the repository you care about. The useful asymmetry is broad freedom inside and a narrow, human-operated path out.

h5i exports three artifacts: a path-validated patch, a human-readable report, and an execution receipt. The box cannot decide that its own result is acceptable. It can propose. A person chooses whether to carry the patch across.

Autonomy inside. Judgment at the boundary.

The boundary has to be cheap

If creating a box is a ceremony reserved for obviously dangerous work, ordinary work remains uncontained. That is why lightweight tiers matter. Under 200 milliseconds changes the decision from “is this risky enough?” to “why would this run anywhere else?”

Stronger boundaries still have a place. A container buys a portable filesystem. A microVM buys a separate kernel. The everyday path and the hostile-code path need not pay the same startup cost, but they should share the same lifecycle and output gate.

A practical test

Ask five questions of any agent sandbox:

  1. Where do package scripts execute?
  2. Which home directory and credentials can they see?
  3. Where does the dev server listen?
  4. Which browser profile opens the page?
  5. Can the agent write the accepted result directly?

If those answers cross the boundary in different directions, the sandbox is smaller than the work.

The boundary has to follow the process tree

A useful sandbox does not ask whether the current executable is called Claude, Codex, npm, cargo, or bash. Names are not security properties. It constrains the process tree that begins with the session.

That distinction matters the moment a tool delegates. A test runner starts workers. A compiler launches a linker. A package manager runs lifecycle hooks. A dev server invokes a bundler, which may invoke a native addon build. If confinement is implemented as a polite wrapper around the top-level command, the first child that does not use the wrapper has left the model.

h5i makes box shell and box run the entry points into a resolved policy. At the kernel tiers, filesystem and syscall restrictions are inherited. At supervised, the session also lives in a private network namespace with its own destination rules. At the image tiers, the whole tree starts inside the container or guest. The agent does not decide which child deserves the boundary. Children get it because they are children.

What escapes when one component stays outside

Component left outsideWhat crosses the boundaryWhy the diff stays quiet
Package managerInstall scripts execute as the host userReads, failed probes, and network attempts need not edit the tree
Dev serverGenerated code and plugins run on the hostThe server may only serve or transmit data
BrowserHost cookies, extensions, downloads, and loopback become reachableBrowser state lives outside Git
Agent configurationAmbient credentials and privileged tool routes enter the sessionAuthority is configuration, not source
Output stepThe subject of review can approve its own resultA direct write looks like any other edit

The table is why “the agent itself is sandboxed” is not enough information. Ask where the work's other interpreters run. Every package hook, compiler plugin, test fixture, web page, and browser extension is another interpreter for input you may not control.

One object gives the work a reviewable lifecycle

Once the environment is the object, the workflow becomes easier to reason about:

  1. Create: freeze the Git base, resolve the profile, and hash the policy before writable state exists.
  2. Work: let the agent edit, build, run services, and use the browser within that policy.
  3. Observe: record process exits and boundary decisions outside the agent's write path.
  4. Review: compare the final tree with the pinned base and read execution evidence beside it.
  5. Export or apply: move one reviewed result across a human-operated gate.
  6. Remove: discard the workspace without turning it into a permanent pet environment.

The frozen base and policy digest are more than metadata. They prevent the meaning of “this run” from drifting. If the parent branch moves or the policy file changes later, the box still names the code and rules it actually started with.

What this design does not claim

A complete boundary can still have a weak tier. workspace gives checkout hygiene and no process confinement. Every tier below microvm shares the host kernel. A container's HTTP proxy cannot constrain a raw socket that ignores it. The boundary is one object; its strength still depends on the mechanism chosen for that object.

Containment also does not certify the patch. A malicious or simply wrong agent can produce code that passes the tests it chose to run. The output gate creates a place for judgment; it does not automate judgment away.

And no local sandbox can stop source from entering a model request that policy legitimately permits. If source must not leave, the answer is a self-hosted model or no model egress—not stronger language around the same allowed API call.

Security becomes normal only when disposal is economical

There is an operational reason integrated environments beat a checklist of wrappers. Developers stop using expensive safety mechanisms for ordinary work. If every agent session requires building an image, negotiating a remote worker, and waiting minutes for dependencies, the box is reserved for code already known to be dangerous. Most supply-chain surprises arrive in code nobody preclassified that way.

The lightweight tiers attack startup cost. Warm caches attack dependency cost without creating a writable rendezvous between boxes: cache content is keyed by lockfile state, populated by a dedicated refresh job, and mounted read-only into agent work. The browser and dev server start inside the already-created boundary, so testing a web change does not require publishing a host port or attaching to a daily browser.

Disposal matters at the other end. A long-lived development container accumulates credentials, caches, debugging exceptions, and manual fixes until nobody can state its boundary. A box has a frozen base, one resolved policy, one purpose, and an expected end. Export what deserves to survive. Remove the rest.

This gives containment a property security tooling rarely gets: the safer workflow is also easier to reason about. One name identifies the workspace, policy, process tree, browser, receipts, and cleanup target. There are fewer ambient pieces for both the agent and the reviewer to misunderstand.

Sources and further reading

Questions that come up

Is a Git worktree an agent sandbox?
No. A worktree separates checkouts and branches. It does not constrain the process tree, filesystem reads, credentials, sockets, network destinations, or browser state.
Why does the browser need to be inside?
The browser executes untrusted page code and holds session state. Keeping it beside the dev server gives both the same isolated localhost while preventing the agent from inheriting a user's normal browser profile.
Read next

Five tiers, five promises

Choose an isolation mechanism by the threat it changes.

Try the whole loop once

Create a box, do one real task, and review the patch beside the execution record.