Essay / Sandboxed workflow · 2026-08-21

Sandbox the entire workflow: browse, develop, review, apply

A browser and a coding sandbox are not two adjacent workflows. Put the browser, checkout, agent, tools, and dev server in one box; review what crossed that boundary before you apply the result.

A coding agent rarely stays in an editor. It reads documentation, installs dependencies, runs tests, starts a local application, opens that application in a browser, follows an error back into the code, and tries again. Sandboxing only the shell while the browser runs on the host splits one job across two security boundaries.

That split is easy to miss because browsing and code development have different interfaces. They are still one authority problem. The page can influence the agent. The browser can hold cookies and reach network destinations. The dev server can expose the code the agent just changed. If one of those pieces sits outside the box, the workflow is only partly contained.

The claim. Create the boundary before the work starts. Put the browser, checkout, agent, toolchain, tests, and dev server inside the same named box. Keep the evidence and the decision to export or apply outside it.
PhaseInside the boxOutside the box
BrowseFresh browser profile, page state, cookies, network clientPolicy, request observation where the tier supports it
DevelopCheckout, agent, package scripts, tests, dev serverCredential broker and resolved policy
ReviewThe proposed tree stays unchangedHuman reads diff, denials, commands, and browser evidence
ApplyNo direct write path to the parent repositoryHuman exports or applies the accepted patch

1. Start with one boundary

Create a named box from the repository before opening the browser or starting the agent. The browser profile supplies a fresh browser identity and the control path needed to operate it inside the box.

repository root
$ h5i box --profile browser --engine h5i --isolation process --name fix-auth
$ h5i box status fix-auth

Creation freezes the base revision and resolves the policy before the workspace exists. status tells you which isolation tier the host actually provided, which paths are writable, how network access is scoped, and the digest that later receipts carry. If the requested policy cannot be enforced, creation fails instead of quietly substituting a weaker tier.

The source determines the output path. A box made from the current repository is a worktree on its own branch, so an accepted result can later be applied locally. A URL, pull request, or --new produces a detached box. Detached work can be exported as a patch, but apply and rebase refuse because that box has no authority over the parent repository.

TierWhat confines the codeEgress scoping
workspaceA separate worktree, no confinementnone
processLandlock, seccomp, namespaces; a supervisor and a private pid namespacedeny or host
supervisedThe above plus a private netns and a seccomp-notify gate on socket()L3/L4
containerRootless Podman on a portable imageL7 proxy
microvmA guest with its own kernel, booted by microsandboxL3/L4 in the guest

The example names process because it can hold a resident browser without a heavyweight runtime. The tier changes what “inside” proves. A process-tier browser is confined for files and environment, but its request log remains the engine's own account. A tier with egress enforcement outside the browser can add host-observed network evidence. Resident browser sessions also need a tier that can keep the engine alive; on Linux today, a microVM is the tier that provides both residence and a network boundary. Read status rather than inferring guarantees from the word sandbox.

2. Put browsing inside the same box

This flag connects the two workflows:

host
$ h5i browser open https://docs.rs/ --allow docs.rs --in fix-auth
ok  browser session br_7k2xqa
   placed   : box fix-auth

$ h5i browser snapshot
$ h5i browser click @e3
$ h5i browser requests

--in fix-auth places the browser engine and its fresh profile in the named box. Later browser verbs address the same resident session, so the page state, cookies, downloads, and requests stay with the development environment instead of appearing in a host browser profile.

The distinction matters in both directions. External documentation can contain instructions that influence the agent, so it should not gain more filesystem or network authority than the coding task. Later, when the browser opens http://localhost:3000, loopback should mean the dev server inside the box—not some unrelated service on the developer's machine.

The browser still checks its origin policy before every request and records the decision before bytes move. Denied requests and refused redirects remain in the log. When the box tier enforces egress outside the engine, h5i can label that traffic host-observed. Without an outside network observer, it remains engine-claimed. Placement and evidence strength are related, but they are not the same claim.

3. Develop and verify without crossing the boundary

Enter the same named box for the coding session:

inside fix-auth
$ h5i box shell fix-auth
box$ claude                          # or codex
box$ npm ci
box$ npm test
box$ npm run dev &

shell inherits stdio, and every descendant stays inside the resolved policy. The agent does not have to remember to wrap package-manager hooks, compiler workers, test processes, or the dev server separately. They are contained because they are children of the box session.

Now point the already-contained browser at the application. From a second host terminal:

browser in the same box
$ h5i browser open http://localhost:3000 --in fix-auth --session app --new
$ h5i browser snapshot --session app
$ h5i browser requests --session app

The browser and server meet on the box's loopback. The useful loop is now continuous: the agent edits, tests, starts the app, reads the page, inspects failed requests or console errors, fixes the code, and tests again. There is no host-browser detour in the middle.

If the agent invokes the browser from inside an existing box shell, it opens the session without --in; it is already in the box. The flag is for a host-side command that places a browser into a box the caller stands outside. h5i refuses --in from inside rather than pretending to add a second boundary.

Model API keys remain on the host. A reverse proxy injects the right key into outbound model requests and scopes it to the runtime, so a Claude box cannot obtain the OpenAI credential. The box gets a copy of the agent's HOME state with credential-shaped entries removed.

The human can observe the page without moving the browser back onto the host:

watch
$ h5i box view fix-auth          # the box's page, on a loopback-only forward
$ h5i box view fix-auth --term   # draw it in this terminal instead
$ h5i ui                         # the whole fleet, read-only, every route a GET

Frames cross outward through the viewer. The browser profile, page execution, and network identity do not. If a human takes control, the control transfer is recorded and stale page handles are invalidated before the agent resumes.

4. Review the whole run, not only the diff

export
$ h5i box diff fix-auth                    # against the pinned base
$ h5i box export fix-auth --out ./review
  wrote ./review/patch.diff, ./review/report.md, ./review/receipt.json

$ $EDITOR ./review/report.md              # read this first
$ git apply --3way ./review/patch.diff

The patch answers what changed. It does not answer which tests ran, what the browser reached, what the boundary refused, or whether a human changed page state during the run. report.md brings those records together without flattening their sources.

Start with denied egress and unavailable evidence. Then read commands with their lanes and exit codes, browser requests and errors, control handovers, and finally the agent's proposal. The proposal comes last because it is testimony from the subject of the review, not an outside observation.

Export is an output gate, not another agent command. The box cannot write ./review; h5i writes the validated bundle from outside after the human asks. Reviewers can carry the patch elsewhere with git apply --3way, which is mandatory for detached boxes.

5. Apply only the result you accept

local box only
$ h5i box status fix-auth          # check base drift and evidence gaps
$ h5i box apply fix-auth           # land the reviewed proposal

apply is available only when the box was created from the current repository. It is never an automatic final step. If the parent branch moved, status names the drift; you can rebase the box deliberately, export the patch, or decline the work.

This is where keeping the workflow in one boundary pays off. The reviewer is not reconciling an uncontained browser history with a sandboxed shell and an agent-authored summary. The code, browser behavior, request decisions, and executions belong to one named run, and the parent repository changes only after that run has been examined.

Cleaning up

lifecycle
$ h5i box ls                  # every box on this clone
$ h5i box status fix-auth     # policy enforced, evidence, base drift
$ h5i box rebase fix-auth     # re-pin onto the parent's current tip
$ h5i box abort fix-auth      # stop, preserving it for forensics
$ h5i box rm fix-auth
$ h5i box gc                  # reclaim finished workspaces

abort stops the box but keeps its evidence. rm deletes it. Keeping those actions separate matters most when a run has done something surprising.

Making it cheap enough to do constantly

A sandbox saved for obviously dangerous jobs will miss ordinary dependency installs, where supply-chain surprises often arrive. The boundary needs to be cheap enough for routine work.

The lighter tiers start quickly. Dependency caches are shared carefully: each project and package ecosystem gets a cache keyed by its lockfile digest. Agent boxes mount it read-only. Only a box with no agent inside may update it, so two agents never meet through writable cache state.

cache
$ h5i box cache refresh npm
$ h5i box cache ls            # which are stale, and therefore unused

A test you can apply to any agent sandbox

You can ask the same questions of any tool that claims to contain an agent:

  1. Where do package install scripts execute, and under which home directory?
  2. Does the browser run inside the same boundary as the checkout and dev server?
  3. When the browser opens localhost, whose loopback does it reach?
  4. Who wrote the record of what ran: the thing being reviewed, or something outside it?
  5. Is a refused action recorded, or does it simply not appear?
  6. Can the agent write the accepted result directly, or does a person carry it across?

If one of those answers points outside the boundary, part of the job is still running uncontained.

What this does not claim

Containment can stop the agent from touching your host. It cannot stop private source from appearing in a model request that policy already allows. If the source must not leave, use a self-hosted model or disable model egress.

Every tier below microvm shares the host kernel. Those tiers are meant for runaway agents and careless dependency code, not a targeted kernel exploit. The microVM tier moves the boundary to a hypervisor.

A receipt is protected from the box, but the host owner can still alter it. It answers a narrow question: could the agent have written this record? It does not prove that the person presenting the receipt left it untouched.

Sources and further reading

Questions that come up

Does --in create the box?
No. Create the box first, then pass its name to h5i browser open --in. The browser session runs inside that existing box and uses its resolved policy.
Can an agent already inside the box use --in?
No. It opens the browser without --in because it is already inside the boundary. The --in flag is for a host-side command placing a browser into a named box.
What is the difference between export and apply?
export writes patch.diff, report.md and receipt.json to a directory and touches nothing else, so you decide what happens next. apply lands the work directly on the parent repository and is only available when the box came from that repository. On a detached box, created from a URL, a pull request or --new, apply refuses and points at export.
Why sandbox it

Why sandbox the entire AI agent workload

See why dependencies, tools, tests, servers, and browsers need one boundary.

Start with one box

Run h5i box probe, create a box, and inspect what the host actually enforced.