Essay / The loop · 2026-08-21

Browse, contain, work, export, apply

The loop is not five commands that happen to compose. It is one property expressed five times: at every step the record is written by something other than the thing being reviewed, and there is exactly one door out, operated by a person.

The claim. An agent session should be reviewable without trusting anything the agent wrote. That single requirement decides the whole shape: a request that is not in the log did not happen, and nothing comes out that a person has not read.
Five steps left to right, browse, contain, work, confer and export, each with the record it leaves behind, above an output gate a person operates
Each step is chosen for what it leaves behind. The last one is the only path back to your repository.

The familiar way to make an agent safe is to stand in front of it. A prompt before each command, an allowlist of tools, a rule file describing what it must not do. Then, at the end, the agent writes a summary of what it did and you read that.

Both halves of that arrangement are authored inside the loop. The prompt is answered by a person who has seen a hundred of them that afternoon and is now answering by reflex. The summary is written by the subject of the review. Neither is dishonest. Both are simply the wrong observer.

So the loop below is built around a different question. Not "what is the agent allowed to do", which is a policy question and a hard one, but "who wrote down what happened, and could the agent have changed it". Everything else follows.

1. Install

One binary. It works on Linux and macOS, which confine by different means: Landlock, seccomp and namespaces on Linux, Seatbelt on macOS. Two optional runtimes add tiers on top of either.

install
$ curl -fsSL https://h5i.dev/install.sh | sh
# or from source
$ cargo install --path .

Then tell your agent how to use it. The skill is embedded in the binary, so it can never document a version you do not have.

skill
$ h5i skill install     # writes into ~/.claude/skills/h5i (or ~/.codex)
$ h5i box probe         # what this host can actually enforce

Run the probe before you rely on anything. It executes a functional self-test rather than reading capability bits, because a hardened kernel or an AppArmor profile can deny confined exec while Landlock, seccomp and user namespaces all report present. The difference between a bit that is set and a boundary that holds is the whole reason the probe exists.

2. Open a browser session

A session is the entire agent-facing surface: one page state, one cookie jar, one request log, one policy. open makes one, every verb that follows acts on it, close ends it. Nothing else is a concept the agent has to learn.

a session, on this machine
$ h5i browser open https://docs.rs/ --allow docs.rs
ok  browser session br_7k2xqa
   placed   : this machine (no containment beyond the engine)
   requests : engine-claimed (fail-closed, and the engine's own account of what it fetched)

$ h5i browser snapshot      # outline, with @ref handles
$ h5i browser click @e3
$ h5i browser requests      # refusals included

That runs here, in your ordinary process space, and h5i says so on the placement line rather than letting the word browser imply a boundary you do not have. What it gives you without one is the record: the engine is the HTTP client, so it checks the policy, writes the decision, and only then touches the wire. When the record cannot be written the fetch is refused. There is no path that reaches the network quietly.

Read the log the way you read a receipt. A denied request is in it with its reason, so the log shows what was attempted and not only what succeeded, and a redirect out of the allowlist is refused at the hop rather than followed and explained afterwards. That is the first instance of the property: the observer is the client itself, and it is arranged so that failing to observe means failing to act.

The label matters as much as the log. h5i calls this lane engine-claimed, because a browser describing its own traffic is testimony, however honest. Step 3 is what upgrades it.

Sessions end, and the ending is written down. A verb sent to a session that is not live is refused with exit code 69 and never silently restarted. An agent whose retry cannot tell "the session is gone" from "the click did not work" quietly starts a second browser and loses both the page it was reasoning about and the record of losing it. --restore carries the old storage into a new id, with the inheritance recorded; an id is never reused.

3. Make a box

Where the code comes from decides the shape of the box, and the difference matters more than the syntax suggests.

create
$ h5i box .                          # this repository at HEAD
$ h5i box --pr 1234                  # a pull request head
$ h5i box https://github.com/o/r     # an external repository
$ h5i box --new                      # empty; the agent builds from nothing

This repository gives you a real git worktree on its own branch, sharing the object store, which is what lets h5i box apply land the work back locally. A URL, a pull request, or --new gives you a detached box: its own repository, your repository neither read nor written after creation, and the inherited origin remote dropped so the box arrives holding no network handle. apply and rebase refuse there and point at export. External code should always arrive in that shape.

At creation the policy is resolved, written to policy.resolved.toml and hashed before any state exists on disk, so a request the host cannot satisfy fails closed rather than leaving half a box behind. The base revision is pinned immutably at the same moment. Those two facts are what stop the meaning of "this run" from drifting: if the parent branch moves or the policy file is edited later, the box still names the code and the rules it actually started with.

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

auto is the default and picks the strongest tier this host can run. Naming a tier explicitly makes it fail closed rather than downgrade, which is the behaviour you want, because a silent downgrade puts a claim in the record the run never had.

Adding --in to h5i browser open places the session from step 2 inside the box, and every verb works unchanged. What changes is the requests line: the egress allowlist is now enforced at the box boundary, outside the browser being described, so the lane goes from engine-claimed to host-observed. Being inside a box does not earn that on its own. A box whose policy lets the browser reach the whole network corroborates nothing, and h5i keeps calling that session engine-claimed.

4. Put them on a forum

One box is a solved problem. The moment a second agent needs to see what the first found, the usual answer is a shared credential: an issue tracker, a chat API, a directory both can write. Each of those turns one compromised agent into two. h5i moves the information instead, and leaves the authority where it was.

forum
# the human, on the host
$ h5i forum create "fix the auth refresh race" --ceiling agent
$ h5i forum attach alpha --as alpha-worker  --role worker
$ h5i forum attach beta  --as beta-reviewer --role reviewer

# the agent, inside its box, with no forum credential to lose
box$ h5i forum list                          # what is open
box$ h5i forum read <thread>                 # read it, posts numbered
box$ h5i forum post <thread> --kind FINDING "..."
box$ h5i forum up 3                          # agree without restating
box$ h5i forum wait                          # block until a peer replies

The --ceiling is the part to get right. It names a profile every participant must be confined under, and attach checks the box's enforced policy against it: the digest-verified policy.resolved.toml, not a worktree file an agent could have edited. Network mode and egress, secret grants, authenticated egress, filesystem read and write grants, AF_UNIX, loopback ports, host-side secret extractors. A box that exceeds any of them is refused. Silently re-confining it to fit would leave its operator believing it has authority it no longer has, and would make "attached" stop meaning "runs the way you configured it". attach also refuses a workspace-tier box outright, because that tier enforces nothing.

Inside a box there is no forum to attack. The two forum-shaped holes both already existed: a read-only inbox the host rewrites, and the single writable spool that was already drained after every session. No socket, no port, no token. The staged record carries a thread, a kind, a body and attachments, and has no field for a sender. The host stamps identity, role, box and policy digest from the environment the record was found in, which is why h5i forum read can draw the host's knowledge above each fence and one agent's claim inside it.

To bring in an agent on another machine, point the forum at a repository. Forum state is Git refs under an append-only union merge, so a deleted conversation returns from any honest clone on the next sync, and --branch-refs publishes it where the forge's branch protection applies. Posts that arrived from a machine this host cannot vouch for are labelled peer-claimed rather than host-observed, because a remote post's identity is testimony and not evidence.

remote
$ h5i forum remote git@github.com:you/agent-forum.git
$ h5i forum remote --branch-refs   # publish under refs/heads/h5i-forum/
$ h5i forum status                 # threads, members, ceilings
$ h5i forum revoke beta-reviewer   # leaves that box's inbox at once

Revocation is immediate, and refusals are recorded rather than swallowed. If a revoked box keeps staging posts, they land carrying the refusal instead of disappearing, and a refused post moves no state: a refused CLAIM claims nothing. A forum that silently drops what it refused teaches its readers that nothing was refused.

5. Work in it

work
$ h5i box shell fix-auth
box$ claude                          # or codex; this is the agent-in-box
box$ npm ci && npm test
box$ npm run dev &
box$ agent-browser open http://localhost:3000
box$ exit

shell inherits stdio, so every command the session spawns is contained by the box rather than by the agent choosing to wrap each call. That is the difference between confinement that holds and confinement that depends on cooperation. A test runner starts workers, a compiler launches a linker, a package manager runs lifecycle hooks; none of them consult the agent about whether they deserve the boundary. They get it because they are children. For a single non-interactive command, h5i box run <name> -- cargo test does the same and passes the exit code through.

No credential goes in. The model API key stays on the host and a reverse proxy injects it into outbound requests, scoped per runtime, so a Claude box cannot reach the OpenAI credential. The per-box HOME state is a copy of your agent's config with credential-shaped entries stripped at any depth.

Watch it work, and take over when you want to:

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

6. Export, read, apply

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

report.md is ordered by how much you should trust each section. Denied egress attempts come first, because a box that tried to reach a host the policy refused is the most interesting thing a review can contain, and it was observed host-side by the allowlist proxy rather than reported by anything inside the box. Then every command with its lane and exit code, then what the page said back, then whether a human took the controls, and last the agent's own proposal, because that is the only section written by the thing being reviewed.

That ordering is the whole essay in one file. Nothing is hidden, but the sections a person reads first are the ones the box could not author, and the section it did author is at the bottom where a summary belongs.

For the local case, where the box came from this repository and landing it here is what you meant, h5i box apply fix-auth does it in one step. It refuses on a detached box.

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 and rm are separate verbs on purpose. Stopping a box that has done something surprising and deleting it are different intentions, and a tool that merges them loses the evidence exactly when it becomes worth having.

Making it cheap enough to do constantly

A boundary reserved for obviously dangerous work leaves ordinary work uncontained, and most supply-chain surprises arrive in code nobody preclassified as dangerous. So the cost of the loop is a security property, not a comfort.

Startup cost is attacked by the lightweight tiers. Dependency cost is attacked by warm caches, without creating a writable rendezvous between boxes: one cache per project and ecosystem, keyed by lockfile digest, mounted read-only into agent boxes, and written only by a box with no agent in it.

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

The loop above is one answer. The questions behind it are portable, and worth asking of anything else that claims to contain an agent:

  1. Where do package install scripts execute, and under which home directory?
  2. Which browser profile opens the page the agent was told to read?
  3. Who wrote the record of what ran: the thing being reviewed, or something outside it?
  4. Is a refused action recorded, or does it simply not appear?
  5. Can the agent write the accepted result directly, or does a person carry it across?

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

What the loop does not claim

Containment stops the agent touching your host. It does not stop it putting private source into a model prompt, which is a separate control: if source must not leave, the answer is a self-hosted model or no model egress, not stronger language around the same permitted API call.

Four of the five tiers share the host kernel. That is strong against a runaway agent and careless dependency code, and it is not a claim against a targeted kernel exploit. microvm is the tier where the boundary is a hypervisor.

And a receipt is protected from the box, not notarized against the host owner. It answers "could the agent have written this", which is the question a reviewer of agent work actually has. It does not answer "could the person showing me this have written it", and h5i does not pretend otherwise.

Sources and further reading

Questions that come up

Do I have to use the browser step?
No. The five steps are independent commands, not a pipeline. Plenty of tasks are a box, a shell and an export. The browser step matters when the agent has to read the web, because that is the step where a page's content enters the session.
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.
Is a box a container?
Only on the container tier. workspace is a worktree with no confinement, process and supervised are kernel-level confinement of a process tree, container is rootless Podman, and microvm boots a guest with its own kernel. h5i box probe reports which of them this host can actually run.
Why does the forum have no sender field?
Because a sender field would be a claim the agent writes about itself. The staged record carries a thread, a kind, a body and attachments; the host stamps identity, role, box and policy digest from the environment it found the record in. That is why forum read can print what the host knows above each post and what the agent claims inside it.
Read next

The environment is the sandbox

Why the unit of isolation is the whole development environment and not the risky command.

Start with one box, then add the second

h5i box probe to see what your host can enforce, then h5i box . When one agent is not enough, h5i forum create is the next command and not a second credential.