The Agent Sandbox With a Review Loop
Letting an agent work unattended is two bets at once: that it can't reach anything it
shouldn't, and that you can tell, afterward, what it actually did. h5i's
env makes those one reviewable unit: an isolated git worktree under a pinned
policy, where every run is captured as evidence and the change reaches your branch only
when you apply it.
- An env is a triple fusion of a code branch, a reasoning branch, and a pinned policy manifest, not just a worktree.
- The lifecycle is human-gated: create, run, diff, propose, apply; apply is the only verb that touches your branch, and it refuses an unproposed env.
- propose is a mediated commit (a canonicalized path allowlist), every run is a secret-redacted evidence capture, and compare is the arena.
A coding agent has crossed a line that autocomplete never did: it runs unattended. It installs
packages, runs builds, edits files across a tree, and reaches the network, for minutes or
hours, with whatever access your shell has, while you do something else. When you come back, the
question is no longer "is this line correct." It's two harder questions at once. What could this
process have touched that wasn't the task, your ~/.ssh, your tokens, an arbitrary
host, if a poisoned dependency or a prompt-injected README nudged it off course? And separately:
can you actually tell, after the fact, what it did do?
Those two questions, blast radius and reviewability, are usually handled by two different
tools that don't know about each other. A git worktree isolates the checkout. A
container isolates the process. Neither, on its own, gives you the thing autonomy
actually needs: a single unit you can hand an agent, that confines it, records what it did, and
then makes its output reach your real branch only when a human says so. That unit, the
confinement-and-provenance core of an auditable
workspace, is what h5i calls an env.
An environment is more than a branch
You already know the worktree half of the story: git worktree add gives a branch
its own checkout so an agent can't trample your working tree. What it doesn't give you is
confinement (the agent still runs with your filesystem and your network), provenance (why each
change was made), or a gate (the branch merges like any other). An env fuses three
things under one name to close all three gaps:
- A code branch:
refs/heads/h5i/env/<agent>/<slug>, checked out into a native git worktree under.git/.h5i/env/…/work(locked for the env's life). The base revision is frozen atcreate, so "what changed" is always measured against a fixed point, not a moving branch. - A reasoning branch:
refs/h5i/context/env/<agent>/<slug>, a versioned record of the agent's own observations, decisions, and notes that rides alongside the code (see the context DAG). The diff tells you what changed; this tells you why. - A policy manifest: the isolation tier and a fail-closed set of filesystem, network, resource, and tool limits, pinned at creation. The policy is content-hashed, and that digest is stamped onto every piece of evidence the env produces, so the run and the rules that governed it can't drift apart.
That triple of code, reasoning, and policy is the whole point. It is the difference between "the agent made a branch" and "the agent worked inside a confined, self-documenting box whose output I can review as one thing." Everything below is the lifecycle of that box.
A worked lifecycle: create → run → propose → apply
The env lifecycle is create → run → propose → apply | abort → gc. Here is the whole
arc on one screen; each verb is unpacked below.
$ h5i env create fix-auth # freeze base, fork code + reasoning branch, pin policy created env · isolation: supervised · profile: agent-claude · worktree under .git/.h5i/… $ h5i env shell fix-auth # hand the box to an agent (or work in it yourself) box$ cargo build && cargo test # every command here is confined + captured box$ exit $ h5i env diff fix-auth # the change, vs the frozen starting point $ h5i env propose fix-auth # mediated commit + review brief (apply refuses without it) $ h5i env apply fix-auth # merge onto your branch — only when you choose to
create freezes the base commit, forks the code and reasoning
branches, and pins the policy. With no --profile it auto-picks the creating
runtime's agent-in-box profile where the host can enforce it, else a fail-closed build/test
profile; --from <rev> bases it somewhere other than HEAD, and
--isolation requests a specific tier (more on that below).
shell drops you, or the agent, into the box with a real,
inherited terminal, so every command the session spawns is confined by the box, not
only the ones someone remembered to wrap. For a single non-interactive command, h5i env
run fix-auth -- cargo build does the same confined execution and captures the output. The
distinction matters for autonomy: shell is how you let an agent live in the box and
run an open-ended sequence of commands under one policy.
diff (add --stat for a summary) shows the work against
the frozen base: the proposed change, before it touches anything real. propose
commits the worktree through a path-checked mediation step and writes a review brief; it never
writes the parent branch. apply is the only verb that touches your
branch, and it refuses to run on an env that hasn't been proposed. It merges by default
(fast-forward when it can) or squashes with --patch. If the work is a dead end,
abort parks the env (manifest and workspace preserved for
forensics) and gc later reclaims the worktree; rm
removes an env entirely. The asymmetry is deliberate: producing a change is cheap and reversible;
landing one is an explicit, human-gated act.
Why propose is a mediated commit, not git commit
It would be simpler to let the agent git commit in the worktree and call that the
proposal. h5i doesn't, because a commit is an attack surface: a worktree is just a directory, and
a directory can contain a nested .git, a symlink that points out of the tree, or a
.. path that resolves to your real repo. propose runs the commit
through a canonicalized path allowlist rooted at the worktree ($WORK). It rejects a
nested .git, refuses symlinked-directory escapes and .. traversal, and
will not accept an agent-introduced or re-pointed gitlink unless it round-trips a registered base
submodule unchanged, with the same path and the same object id as the env-branch tip. Anything that resolves
outside the box fails closed. The result is that the bytes you review in diff are
the bytes that get committed, with no path trickery in between.
Isolation is a tier, and the post is honest about it
"Confined" means different things on different machines, so the policy names an isolation tier
and h5i env create with no flag picks the strongest one the host can actually
enforce. Crucially, an explicit tier fails closed: ask for a level the host can't
provide and create refuses rather than running with less. A bits-present
check isn't enough, either: create runs a functional self-test before it trusts a
kernel tier (on some hosts, e.g. AppArmor-restricted user namespaces on CI, the bits exist but
confined exec doesn't work).
| Tier | What confines the code | Network |
|---|---|---|
| workspace | git worktree only, no process confinement, for trusted code | host (unrestricted) |
| process | Landlock filesystem allowlist + a seccomp syscall deny-list + user/mount/pid/net namespaces + rlimits, all rootless | off, or full host |
| supervised | the process tier + a live seccomp socket gate | off, or an L3/L4 egress allowlist |
| container | rootless Podman, --cap-drop=ALL, no-new-privileges, read-only rootfs, no docker socket | an L7 egress allowlist (CONNECT proxy) |
h5i env probe prints exactly what your host supports. The two egress mechanisms are
different, and worth choosing between: the container tier runs an
HTTP/HTTPS CONNECT proxy and points the box at it: honest L7 scoping that blocks
proxy-respecting tooling but not a program that opens a raw socket to a permitted IP; the
supervised tier installs a default-drop nftables ruleset in the box's
own network namespace over a rootless slirp4netns uplink, which stops the packet at
L3/L4 regardless of how the program tries to connect. None of this needs root or a VM,
and h5i is clear that it is strong containment for ordinary and opportunistic threats, not a
hardware boundary against a determined kernel exploit. The full mechanism (Landlock allowlists,
the seccomp deny-list, the egress internals, and the microVM tier h5i doesn't pretend to ship
yet) is the subject of its own post:
Sandboxing AI Agents, Part 4: h5i's design.
Every run is evidence; compare is the arena
Confinement decides what the code can do; the captured record tells you what it
did. Every run in an env is a tagged, secret-redacted evidence capture
stored in git refs and stamped with the env id and the policy digest that was in force, so a run
can never be read apart from the rules that governed it. Blocked accesses are recorded too, not
silently swallowed.
$ h5i env status fix-auth # lifecycle, enforced policy, evidence, base drift $ h5i recall objects --env fix-auth # this env's captures $ h5i env inspect fix-auth --capture <id> # one run's full structured record $ h5i env compare a b c # rank parallel attempts side by side
Because the evidence is content-addressed and lives under refs/h5i/env, the whole
env travels with h5i share push / pull. A pulled env has no local
worktree, so diff falls back to base..branch-tip (the proposed state)
and apply works straight from the branch, which is what makes the cross-agent loop
real: one agent proposes a change on its clone, and a teammate or another agent reviews and
applies it on theirs, with the full evidence in hand.
compare is where this pays off for autonomy at scale. Spin up several envs on the
same frozen base, with different agents and different approaches, let each work sealed, and rank them
side by side: the changes each made and the results of each one's last run, in one table. This
"arena" is the substrate the h5i team
ensemble builds on, where the same task is run across personas and a neutral verifier replays
each candidate before any verdict is applied. The shared, hashed base is what makes the
comparison fair.
Failure modes worth knowing
- The host can't enforce your tier. Explicit
--isolationfails closed;createtells you why instead of downgrading. Runh5i env probefirst to see what's available, and remember the kernel tiers need a working user-namespace + Landlock + seccomp stack, which some CI and AppArmor setups disallow. - The base advanced under you. A long-running env can fall behind its parent.
statusreports the drift;h5i env rebasere-pins the base onto the advanced parent with a 3-way merge, and refuses if that would conflict, rather than producing a silently wrong base. - You tried to apply without proposing.
applyrefuses an unproposed env by design; runpropose(which is also where the path-allowlist mediation runs) first. - Egress scoping confusion. A
container-tier allowlist is L7 and only constrains proxy-respecting clients; if you need a raw-socket-proof boundary, choosesupervised(L3/L4). Either way, declarenet.egressin.h5i/env.toml; the staticprocesstier can't honor a domain allowlist and will say so. - You expected total containment. These tiers don't claim to stop a determined kernel exploit; that's the unshipped microVM tier's job. Match the tier to the threat, and don't run hostile code on a kernel tier expecting hardware-grade isolation.
Conclusion: the unit of safe autonomy
The reason an agent is worth confining is the same reason its output is worth reviewing: you
didn't watch it work. h5i's answer is to make those one object instead of two. An env
is an isolated worktree, a record of the agent's reasoning, and a pinned policy, bound together
so that the box that confined the work is the same box that produced the evidence and the same
box you propose and apply from. Nothing reaches your base branch by accident. apply
is a deliberate human act on a change you have already diffed, with the policy that governed it
hashed into the record.
That is a narrower promise than "a sandbox," and a more useful one. It doesn't ask you to trust the agent, and it doesn't ask you to babysit it either. It asks for one decision, at the end, with the evidence in front of you, which is exactly the decision you wanted to keep.
FAQ
How is an h5i env different from just running an agent in a git worktree?
A worktree isolates the checkout but not the process, since the agent still has your filesystem and
network, and the branch merges like any other. An env adds a fail-closed policy, captured
evidence, and an explicit apply gate, so confinement, provenance, and review are
one reviewable unit rather than three loose pieces.
Is the agent actually confined, or just isolated in a branch?
It depends on the tier you can run. workspace is a worktree with no process
confinement; process and supervised add kernel confinement (Landlock,
a seccomp deny-list, namespaces, rlimits), with supervised adding an L3/L4 egress
allowlist; container uses rootless Podman with an L7 egress proxy. h5i env
probe reports what your host can enforce, and create refuses a tier the host
can't satisfy instead of downgrading silently.
Why can't an env's commit step write outside its worktree?
propose is a mediated commit. It enforces a canonicalized path allowlist rooted at
the worktree, rejecting a nested .git, symlinked-directory escapes, and
.. traversal, and refusing an agent-introduced gitlink unless it round-trips a
registered base submodule unchanged. Paths that resolve outside the box fail closed.
Can another person or agent review and apply an env on a different clone?
Yes. Env state lives under refs/h5i/env and travels with h5i share push
/ pull. A pulled env has no local worktree, so diff falls back to
base..branch-tip and apply works from the branch, so one agent proposes
on its clone, another reviews and applies on theirs, with the full evidence.
Give your agents a box, not your shell
Try h5i on your next AI-assisted branch: create a sandboxed workspace, capture the run, and post a review-ready PR brief.
Star on GitHub Back to docs