Command reference for h5i. New here? Read What h5i is and The loop first — they give the mental model before the per-command reference.
h5i <command> --help is always authoritative for flags. This manual explains
what the commands are for.
Give coding agents full autonomy to build and test web apps inside a disposable environment, without exposing your machine or your credentials.
h5i (pronounced high-five) is a contained agentic development environment: a throwaway box that holds the code, the agent, the toolchain, the dev server, and a real browser, with nothing of your machine inside it and nothing leaving it except a patch you reviewed.
It is one Rust binary. No server, no daemon, no SaaS.
Everything h5i does maps to one of these:
- A disposable workspace. The code is copied into the box — a pull request, an existing repository, a fresh project — along with everything it pulls in. No host directory is mounted read-write into the agent's reach.
- A sandboxed coding agent. Claude Code or Codex, its child processes, its package managers, builds, tests and dev server all run inside the same boundary. A runaway agent stays in the box.
- A credential and network broker. No SSH key, GitHub token, model API key, cloud credential or personal browser profile enters the box. A host-side proxy authenticates the calls the policy allows; egress is an allowlist.
- A browser in the box, with two interfaces. Chrome and its profile live inside. The agent drives it through a CLI; a human can watch the same viewport and take over.
- An output gate. At the end you export a patch, a report, and an execution receipt, after inspection. The agent has no direct write path to the host.
The value is not any one of these. It is that code, agent, dev server, browser and export all sit inside one boundary that both the agent and the human can operate.
- Not a provenance system. h5i used to record who wrote what, with git notes, blame overlays and a multi-agent orchestra. That is gone. What survives is containment and the receipt of what actually ran.
- Not a defence against a targeted kernel exploit. See Limits.
h5i box . # a box from this repository
h5i box shell mybox # work in it (this is where an agent runs)
# inside: edit, build, start the dev server, drive the browser
h5i box export mybox --out ./review # patch + report + receipt, for a human
git apply --3way ./review/patch.diff # apply it where you wantThe full loop the browser makes possible:
agent edits code -> starts dev server -> opens the app with agent-browser
-> reads the accessibility tree -> clicks and fills -> reads console and
network errors -> screenshots -> fixes the code -> human watches or takes over
-> export patch, report, receipt
curl -fsSL https://h5i.dev/install.sh | sh # prebuilt binary
cargo install --path . # from sourceThen, so your agent knows how to use it:
h5i skill install # writes the skill into ~/.claude/skills/h5i (or ~/.codex)
npx skills add h5i-dev/h5i # same bytes, if you do not have the binary yet| Group | What it is for |
|---|---|
h5i box |
Create, run, inspect and export boxes. Almost everything. |
h5i ui |
The box console: one read-only screen over the whole fleet. |
h5i browser |
The control lock: who is driving a box's browser. |
h5i skill |
Write or print the agent skill this binary carries. |
h5i completion / h5i man |
Shell completions and the man page. |
h5i dev * and h5i env * both remain as hidden aliases for h5i box *
through one release. The noun the product uses everywhere else is box, so the
command is too.
h5i box . # snapshot this repository at HEAD
h5i box --pr 1234 # a pull request (number, #number, or URL)
h5i box https://github.com/o/r # clone an external repository
h5i box --new # an empty box; the agent builds from nothingh5i box [SOURCE] is shorthand for h5i box create, and
takes the same flags. A pull request is --pr, not a positional: a bare number
is ambiguous with everything else a source could be, and h5i box create
already spelled it as a flag.
Where the code comes from decides the shape of the box:
- This repository → a real git worktree on its own branch, sharing the
object store, so
h5i box applycan land it back locally. - A URL, a PR, or
--new→ a detached box. It gets a repository of its own inside its directory, this repository is neither read nor written after creation, and the inheritedoriginremote is dropped so the box cannot reach a network handle nobody granted it.applyandrebaserefuse and point atexport. This is the shape external code should always arrive in.
h5i box create <NAME> [--from <rev>] [--pr <n>] [--clone <url>] [--new]
[--profile <p>] [--isolation <tier>] [--image <img>]
The base revision is frozen at creation and pinned immutably. The policy is resolved, digested and stored before any state is created on disk, so an unsatisfiable request fails closed rather than leaving half a box behind.
| Flag | Meaning |
|---|---|
--from <rev> |
Base revision (default HEAD). |
--pr <n|url> |
Fetch refs/pull/<n>/head and pin it as the base. Needs only git. |
--clone <url> |
Copy an external repository in. Detached. |
--new |
Empty box (a fresh repository with one empty commit). Detached. |
--profile <p> |
See Profiles. |
--isolation <tier> |
See Isolation tiers. |
--image <img> |
Base image for isolation=container and isolation=microvm. Pre-pulled; runs never pull. |
h5i box ls # every box on this clone
h5i box status <name> # policy actually enforced, evidence, base drift
h5i box run <name> -- cargo test # one command; the exit code passes through
h5i box shell <name> # interactive confined session
h5i box diff <name> # what changed against the pinned base
h5i box log <name> # the box's event logh5i box shell is the agent-in-box: stdio is inherited, so every command the
session spawns is contained by the box rather than by the agent choosing to wrap
each call.
h5i box service start <name> <service> # a declared long-lived process
h5i box service status <name>
h5i box service logs <name> <service>
h5i box ports <name> # the per-box dynamic port mapServices are declared in .h5i/env.toml:
[service.web]
command = "npm run dev"
port = 3000Supported at the workspace and process tiers in v1. At supervised and
container the network namespace belongs to a single session, so run the dev
server inside the same h5i box shell as everything else.
The output gate. A box has no write access to anything outside itself; this is the only way out, and it is deliberately a human step.
h5i box export <name> --out ./reviewProduces:
| File | What it is |
|---|---|
patch.diff |
The tree diff against the pinned base, path-validated: no symlink escapes, no nested .git, no agent-introduced gitlinks. |
report.md |
What ran, what the browser saw, who was at the controls, and the agent's own proposal. |
receipt.json |
Every observed execution, with the policy digest that was enforced. |
It refuses rather than overwrites an existing non-empty directory (--force to
replace). Secret redaction and size caps apply to all three.
Read report.md before applying. It surfaces, in this order:
- denied egress attempts — the box tried to reach hosts the policy refused
- what ran — every command, its lane, its exit code
- what the browser saw — console errors, uncaught exceptions and failed requests, observed by h5i rather than reported by the agent
- viewer sessions — including whether a human took the controls
- the agent's proposal
Then apply it where you want:
git apply --3way ./review/patch.diffh5i box apply <name> still lands a proposed box onto its parent branch in this
repository, for the local case where that is what you want. It refuses for a
detached box.
Cold dependency install is the difference between a 20-second box and a four-minute one, so warm caches are in scope.
h5i box cache ls # caches for this project, and whether they are stale
h5i box cache mounts # exactly what a box would get
h5i box cache refresh <eco> # populate one, in a dedicated box with no agent in it
h5i box cache rm <eco>Rules that make this safe rather than merely fast:
- One cache per project and ecosystem, keyed by a digest of that ecosystem's lockfiles. A cache whose key no longer matches is listed as stale and never handed to a box: packages resolved for a different dependency set are a silent, hard-to-explain wrong answer.
- Mounted read-only into an agent box. That costs nothing in correctness — every package manager falls back to fetching what it cannot find.
- Written only by
h5i box cache refresh, which runs the install step alone, with egress narrowed to the registry hosts and no agent inside.refreshneeds a project-declared profile whose egress is the registry hosts and nothing else, and it refuses with that profile written out ready to paste rather than creating a box whose fetch could not have worked.
No mutable surface is ever shared between an agent box and anything else.
Watch a box's browser from the host, and take over when you want to.
h5i box view <name> [--port 7331] # serve it to your browser
h5i box view <name> --term # draw it in this terminalThe box has to be running (a live h5i box shell or h5i box run), and its
browser has to be streaming (agent-browser stream enable, inside the box).
This is a security boundary, not a convenience, so it is worth knowing what it is. The box's stream port is never published: it stays inside the box's private network namespace, and h5i enters that namespace by pid, connects from inside, and hands the socket back out. Then:
- the forward binds loopback only, on a port h5i chose
- every connection carries a per-box token, minted at creation and never written anywhere the box can read
- cross-origin WebSocket handshakes are refused, so a page you have open in another tab cannot reach a running box
- frames flow out always; input flows in only for the control-lock holder
--term draws the page in the terminal you are already in, next to the agent.
It also works over SSH, which the browser viewer does not without forwarding a
port. It needs a terminal that speaks the Kitty graphics protocol: kitty,
Ghostty, WezTerm and Konsole all do. When yours does not, h5i says so and points
you at the browser viewer rather than filling the screen with base64.
This path binds nothing at all. There is no port, no token, and no page: the viewer runs inside the command you just typed, enters the box the same way the forward does, and holds the socket itself. Nothing else on the host can connect to it, so there is nothing to authenticate.
The other direction matters as much. A terminal's output is not text: an escape sequence can rewrite your clipboard, retitle the window, or ask the graphics protocol to read a file. So the box never writes to your terminal. It supplies compressed pixels inside a WebSocket message, and every escape sequence you receive is generated by h5i.
Row one is h5i's, and a page cannot draw on it or be clicked through it. It
carries the box, the mode, who holds the control lock, the page's origin, the
egress policy, and a count of console errors. The origin is the field that never
gets shortened at the expense of the truth: a long URL loses its path, and a
host too long for the row is cut from the left, because shortening
bank.example.evil.test from the right is the trick itself.
Two modes:
| Key | Does |
|---|---|
i |
Take the control lock and start driving the page |
Ctrl-] |
Hand control back and return to watching |
q |
Leave |
Watching is read-only and leaves the mouse to your terminal, so selection and
scrollback still work. Driving takes the lock, because in a terminal viewer
there is no second window to run h5i browser take in. Leaving hands it back.
Both are recorded in the receipt, under the same lane the browser viewer uses.
Two limits worth knowing. A terminal reports key presses and not releases, so h5i sends a press and a release together: typing works exactly, and holding a key down does not. Clicks are placed at the resolution of a terminal cell, which is fine for a form and coarse for a dense canvas.
h5i box probe # what this host can enforce at all
h5i box capabilities <name> --json # what this box actually got
h5i box doctor <name> # can it still enforce its claim? are its refs intact?
h5i box secrets <name> # declared grants, dry-run resolution, never values
h5i box inspect <name> --capture <id>
h5i box compare <a> <b> # boxes side by sideh5i box rebase <name> # re-pin onto the parent branch's current tip
h5i box abort <name> # stop; manifest and workspace preserved for forensics
h5i box rm <name> [--force] # remove entirely
h5i box gc # reclaim applied/aborted workspacesh5i box allow # list the current entries
h5i box allow api.example.comA persistent, user-level egress allowlist merged into every container-tier box
whose profile already sets net.egress. A deny-all profile is never widened.
Stored under ~/.config/h5i/, outside every box-granted path, and it refuses to
run inside a box.
h5i ui # http://127.0.0.1:8765/?token=…
h5i ui --port 0 # let the OS pick the port
h5i ui --open # hand the URL to this desktop's browser tooThe box console: the same fleet the commands above report on, drawn as one
screen. Left is every box with its tier, status and one signal; right is the box
you picked — the policy that was actually enforced, the services it declares,
its diffstat against the pinned base, and a flight recorder with one row per
receipt across five lanes (FS, NET, PROC, RES, PAGE). Click a row for the
rendered receipt, the same text h5i box inspect prints.
It cannot drive anything. Every route is a GET. shell, run, export,
propose, apply and rm stay in the CLI, where a human types them, so there
is no mutating surface to guard and no way to turn the console into a remote
control for someone's boxes.
What guards it. The server binds 127.0.0.1 and nothing else. The URL
carries a token minted for this session and kept in memory — never written to
disk, so no box can read it — which the page trades for a SameSite=Strict
cookie on first load. Requests from another origin are refused outright.
What the colours mean. Red is the only one that makes a claim about the
boundary: the egress allowlist refused a destination, host-observed by the
proxy. Amber is something to look at — a run exited non-zero, the wall-clock
limit killed one, or the in-box browser reported errors — and says nothing about
containment. Grey means the evidence is weak: either the tier is workspace and
nothing was confined, or every receipt came from the in-box shim and so is the
box's own account. Each run row is labelled host-observed or box-claimed for
the same reason. Nothing on the screen is a score.
The console is a default-on cargo feature. cargo build --no-default-features
drops it, along with axum, tokio and the build script's need for Node — and the
h5i ui command with it.
Deliberately four verbs. Driving the browser is agent-browser's job; what h5i
owns is arbitration between the agent and a human, because nothing upstream does
it.
h5i browser status <name> [--json] # who holds control, and whether @refs are stale
h5i browser take <name> # a human takes control
h5i browser release <name> # hands it back
h5i browser url <name> [--port n] # the viewer URL, token includedThe rules:
- The agent holds control by default. A box exists to let an agent work; it should not have to ask.
- A human takes control, never asks for it. Someone reaching for the viewer wants the pointer now, and the agent is a program that can wait. The agent's mutating verbs are refused with a typed message rather than fighting for the pointer; read-only verbs keep working, because watching never collides.
- Handing control back invalidates what the agent knew. The page moved, so
every
@reffrom its last snapshot may now point somewhere else. It must re-snapshot before acting, and acting first is refused rather than mis-clicked.
That is agent-browser, run inside the box, and its --help is the verb
table. h5i does not wrap it: forty automation verbs behind a second CLI would
buy nothing but drift.
agent-browser open http://localhost:3000
agent-browser snapshot # accessibility tree with @refs
agent-browser click @e2
agent-browser fill @e3 "test@example.com"
agent-browser screenshot shot.png
agent-browser stream enable # so `h5i box view` has something to showWhat the browser profile does to Chrome, and why:
-
Fresh profile, created in the box. No host cookie jar, extension or history. Nothing you are logged into on the host is logged in there.
-
Chrome's egress is the box's egress, enforced by the tier. Loopback stays open, because the dev server under test is the whole point.
--allowed-domainsis derived from the same policy as a second layer. -
AI chat is refused.
agent-browser chatsends page content to an external gateway, which inside a box is an exfiltration path with a friendly name. The gateway credential is never injected, and its absence is the mechanism. -
On macOS it is granted the host's per-user temp directory (
/var/folders/<xx>/<yy>/T), read and write. This is the widest thing any profile asks for and it is worth understanding before you use it. Chrome puts its ProcessSingleton lock socket there, and it finds that directory throughconfstr(_CS_DARWIN_USER_TEMP_DIR)rather thanTMPDIR— so the per-env/tmpredirect cannot move it, and without the grant Chrome will not start. The cost is that the directory is shared: a browser box can read what other host processes leave there and can plant files they will pick up. That is exactly the cross-agent rendezvous point the/tmpredirect exists to remove, reintroduced for this one profile on this one platform. Other profiles, and every profile on Linux, are unaffected.Two consequences of that grant being a machine-specific absolute path. The pinned policy digest differs between two Macs for the same profile — harmless, because
policy.resolved.tomlis verified against the digest stored beside it, and both are written together at create time; nothing re-resolves the profile and compares. But abrowserenv created on one machine and pulled to another carries a grant for a directory that does not exist there, so Chrome will fail to start until the env is recreated.
skills/h5i/ is embedded in the binary at build time, so the skill cannot
document flags the installed binary does not have.
h5i skill install [--target <dir>] # write it out
h5i skill show [<page>] # print SKILL.md or one reference page
h5i skill path # where an install would writeThis is also how the in-box agent gets the skill: nothing is baked into an image, and nothing is copied from host to box.
A box's policy is resolved at creation, serialized to policy.resolved.toml,
and digested. Every receipt records the digest that was actually in force,
so "what was enforced" is never a matter of trust.
Built-ins need no file:
| Profile | What it grants |
|---|---|
default |
Fail-closed build/test confinement: system paths read-only, $WORK read-write, no network. |
agent |
The agent-in-box surface, scoped to $H5I_AGENT's runtime. |
agent-claude / agent-codex |
Pin one runtime: only that agent's HOME state and API egress. |
browser |
The agent profile plus headless Chrome and the agent-browser daemon. |
Runtime scoping is not cosmetic: a Claude box must not get Codex's credentials or egress to OpenAI, because a prompt-injected agent could otherwise read the other runtime's token and use it against an allowlisted host.
Custom profiles live in .h5i/env.toml:
[profile.review]
isolation = "supervised"
[profile.review.fs]
read = ["/usr", "/etc"]
write = ["$WORK"]
[profile.review.net]
mode = "deny"
egress = ["api.github.com"]
unix = false # AF_UNIX sockets; see below
loopback = [3000] # macOS only; see below
[profile.review.resources]
mem = "4G"
procs = 256
wall = "30m"wall is enforced everywhere. mem and procs are not enforced at the
process and supervised tiers on macOS: Darwin has no cgroups, does not
enforce RLIMIT_AS against the mmap'd heap every modern runtime uses, and
scopes RLIMIT_NPROC to the whole user rather than to one box, so h5i declines
to apply a limit it cannot hold. h5i box status marks such a value with *
and says so underneath, rather than listing it as enforced. Use
isolation = "container" or isolation = "microvm" where you need a real
ceiling — both cap memory and process count in the runtime itself.
| Tier | What it is | Network scoping |
|---|---|---|
workspace |
No confinement; just a separate worktree. | none |
process |
Landlock + seccomp + namespaces, with a supervisor and a private pid namespace. | deny or host |
supervised |
Everything process has, including the private pid namespace, plus a private netns with an nftables egress allowlist pinned to resolved IPs, DNS pinned by a hosts file, and a seccomp-notify gate on socket(). |
L3/L4 |
container |
Rootless Podman: a portable image, with a CONNECT-proxy egress allowlist. | L7 |
microvm |
A hardware-isolated guest with its own kernel, booted by microsandbox (msb) from the same OCI images. Egress rules are evaluated by the VM's network stack. |
L3/L4 |
auto (the default) picks the strongest tier the host can actually run. An
explicit tier fails closed if the host cannot satisfy it — h5i never
silently downgrades.
Worth being clear about, because two drafts of the design got it backwards: the
container tier buys portability, not tighter network control. Its allowlist
is a proxy, so it binds proxy-respecting tooling only. supervised enforces at
L3/L4 and does not have that hole.
microvm is the one tier where the boundary is a virtual machine rather than a
policy applied to a host process. A kernel exploit inside the box meets the
hypervisor, not the host kernel it just subverted. Its net.egress allowlist
becomes default-deny plus one address rule per allowed destination, so a raw
socket to an unlisted IP is dropped rather than merely un-proxied.
Requirements, all three, or the tier refuses:
- microsandbox's
msbonPATH, version 0.6 or newer. - Host virtualization:
/dev/kvmopenable on Linux, Apple Silicon on macOS. A stock WSL2 kernel and most cloud CI runners have neither. - A base image, from
--image, the profile'scontainer.image, or the repo-level[container] image— the same images the container tier runs, pre-pulled withmsb pull.
h5i box probe reports which of the three is missing, since "install a package"
and "enable nested virtualization" are different problems.
Two things it does not do yet, stated rather than left to be discovered:
- No per-request egress tally. The container tier's proxy sees every CONNECT and records allow/deny counts in the capture manifest. A netstack filter drops packets without reporting them, so a microvm receipt carries no egress summary. Stronger enforcement, thinner evidence.
- No authenticated-egress grants.
[[profile.X.auth]]hands the box a base URL pointing at a credential proxy on the host's loopback, which a microVM guest cannot dial. A profile that declares grants is refused at this tier rather than handed an origin that resolves to nothing.
In-box observation works as it does under container: the read-only
managed-settings mount carrying the wrap-bash hook, and the capture spool at
/.h5i/spool. The container tier's tee shim has no analogue here (it depends on
self-mounting the image, which a VM has nothing to do).
[profile.X.net] unix = true lets the box create AF_UNIX sockets. Off by
default, because SCM_RIGHTS passes file descriptors, which is authority
smuggling.
What the grant does not open, which is why it can exist at all: abstract
sockets are scoped by the box's private netns; filesystem-bound ones are scoped
by Landlock; and /tmp — where .X11-unix, tmux-* and an ssh-agent live — is
a per-box scratch at the kernel tiers. What is left is a host socket sitting
inside a granted path, so the grant is opt-in per profile and pinned in the
digest.
The browser profile sets it, because the agent-browser daemon's control
socket is a filesystem-bound AF_UNIX listener.
-
Model API: the key stays on the host. A reverse proxy injects it into outbound requests from the box, scoped per runtime, so a Claude box cannot reach the OpenAI credential.
-
Any other service: the same mechanism, declared as policy:
[[profile.review.auth]] host = "api.github.com" credential_env = "GITHUB_TOKEN" # read on the host, never in the box base_url_var = "GH_HOST" # what the client reads token_var = "GH_TOKEN" # where the box gets its per-run dummy
token_varis required. The proxy gates every request on a per-run token, so the box has to be handed it in whatever variable its client already sends as a credential. The real credential stays on the host; the box only ever holds the dummy.The limit is real and worth knowing before you declare one: it binds clients you can point at another origin, so a plain
curl https://api.github.comstill goes nowhere. A TLS-terminating forward proxy would lift that, at the cost of a CA the box trusts, and it is deliberately not built.Restricting what the box may do with a credential is authorization, and it belongs where it is already solved: a fine-grained token scoped to one repository and the operations you meant.
-
Per-box HOME state is a copy of the host agent's config, seeded once and never written back, with credential-shaped entries stripped at any depth (
credentials*,.netrc, ssh keys,*.pem/*.key/*.p12) — keeping only the runtime's own token, which it cannot function without.
Declared per profile, brokered host-side, injected for the life of one run:
[profile.review]
secrets = ["DEPLOY_KEY"]
[profile.review.secret.DEPLOY_KEY]
source = "env:H5I_SECRET_DEPLOY_KEY" # the default for a bare name
inject = "env" # `file` is workspace-tier only in v1The value never appears in the policy, the digest, or any receipt. h5i box secrets <name> dry-runs the resolution and reports a fingerprint, never a
value. A grant that cannot be resolved fails the run closed rather than starting
a box that will fail confusingly later.
One append-only JSONL log per box, plus the raw payload of each record. A record is generated from observation, never from the agent's account of itself.
Two properties the design depends on:
- Append only, and sealed. The box's write window under its own directory is
exactly
<box>/spool. The receipt log and the stored payloads are siblings of that spool, outside every grant. The box stages a record; the host ingests it. There is no path from inside to a record the host has already written. - Redacted at the boundary. Secrets are scrubbed from the command and from the payload before either is written, and the scrub is recorded by rule id, never by value.
Every record carries the lane that observed it, so the two kinds of evidence never blur:
| Lane | Who observed it |
|---|---|
host-env-run |
h5i, host-side. Exits and resource usage come from the supervisor's wait4; egress from the allowlist proxy's own log. |
viewer |
h5i's own viewer forward. The box supplies none of it. |
tee-shim |
The box's shell shim. Box-claimed. |
inbox-capture |
Staged by the box. Box-claimed. |
A run that drove the browser also carries what the page said back: console errors, uncaught exceptions, and requests that failed. h5i collects these itself, right after the command, in the same box under the same policy — so the timing is not the agent's to choose. Only what is new since the last drain is recorded.
A browser command with no browser to ask is recorded as unavailable, not as
a clean page. "Nothing was looked at" is a different claim from "nothing was
wrong", and a reviewer has to be able to tell them apart.
An agent can stage extra records, or stop writing. Both are visible: a gap between host-observed exits and box-reported commands is itself a finding. We do not claim more than that.
Being explicit about these is a feature, since the claim is a security claim.
-
The agent can send your source to the model API. Containment stops the agent from touching the host. It does not stop it from putting private code in a prompt. That is a separate control (a self-hosted model, or no model egress at all) and we will not imply otherwise.
-
Shared kernel. Podman and the kernel tiers share the host kernel. Good against a runaway agent and against careless dependency code. Not a claim against a targeted kernel exploit. A microVM backend is the answer, and it is not built.
-
The container tier's egress scoping is L7. Its allowlist is a proxy, so it binds proxy-respecting tooling only.
-
An interactive session at a kernel tier shares your terminal.
box shellhands the box the terminal you launched it from, because that is what makes job control and every TUI work — a box shell is a nested shell, not a connection to somewhere else. A terminal is a two-way device, and the box gets both directions of it, so the residual is a list rather than a single door:-
Typing at your shell (
TIOCSTIpushes characters into the terminal's input queue, which your shell reads as if you had typed them, after the session ends). Whether that is closed is not h5i's to assert, and the two platforms answer from different places. On macOS it is the Seatbelt profile that subtracts the ioctl — so it holds atprocessandsupervised, and not atisolation=workspace, which applies no profile by design, nor on a host whose Seatbelt is unusable. On Linux it is your kernel's setting, the same at every tier, since h5i does no ioctl filtering of its own there: 6.2 made TIOCSTI disableable viaCONFIG_LEGACY_TIOCSTIanddev.tty.legacy_tiocsti, but upstream defaults that open — many distros ship it closed, and a kernel older than 6.2 cannot close it at all. So h5i measures instead of claiming.h5i box probeprints one of:tty-injection= blocked at every tier tty-injection= blocked at the kernel tiers, possible at isolation=workspace tty-injection= possible at every tierand, when anything is open, how to close it.
-
Reading what you type next. The session's read grant on the terminal is not revoked when the shell exits, so a box process that outlives the session — a stray background job — can read the terminal it still holds open. This predates the tty ioctl grant; it is a property of sharing the device.
-
Leaving the terminal in a state. A box can set raw mode, turn echo off, change the line discipline, or take the terminal exclusive so other programs cannot open it. Recoverable (
stty sane, or a new terminal), not an escape, but it is yours to recover.
What is not reachable, checked rather than assumed:
TIOCCONS(redirecting console output to the box's terminal) is refused by Darwin for a non-root process with or without a sandbox. Giving the box its own pty and proxying it is the fix that ends the whole list, and it is not built. The container and microVM tiers do not share a terminal at all. -
-
Chrome runs with its own sandbox off. On Linux, h5i's seccomp deny-list blocks the namespace syscalls Chrome's sandbox needs, at every tier. h5i's box is the boundary; Chrome's is not available inside it. That is one layer fewer than a browser on the host has.
-
A dev server the box runs is reachable only if its port is declared. On macOS the box shares the host's loopback, so h5i denies outbound to it wholesale — otherwise a box could reach a database or a dev server belonging to the host. A box that runs its own dev server and wants to point its own browser at it names the port:
[profile.X.net] loopback = [3000]. Exactly that port is granted; everything else on loopback stays denied, and an undeclared port fails withnet::ERR_ACCESS_DENIED. The port a declared[service]is running on is granted automatically while that service is alive, so this is only needed for a server started by hand. On Linux the box has its own network namespace and none of this applies. -
On macOS the browser has no in-process domain check. agent-browser cannot start Chrome from inside a Seatbelt sandbox — the failure reproduces under a fully permissive
sandbox-execprofile and disappears without the sandbox, so it is not something a grant fixes. h5i therefore launches Chrome itself and attaches agent-browser to it with--cdp, which upstream refuses to combine with--allowed-domains. So that flag is not set for a macOS browser box: the tier's own egress enforcement is unchanged and still the boundary, but agent-browser's second, in-process domain list is gone. A page on a non-allowlisted host fails inside Chrome withnet::ERR_ACCESS_DENIED. -
A browser box's Chrome is restarted when its route out changes. Chrome outlives the run that started it, and it takes its proxy address once, at launch — so a browser started before the box's current route (an upgrade, or a run whose proxy port moved) cannot reach the network through it. The box cannot restart it itself: a browser from a previous run is in a previous sandbox instance, which Seatbelt's same-sandbox signal grant does not reach. It is detected in the box and stopped host-side at the start of the next run, so the fix costs one extra run and says so rather than failing with a proxy error that reads like a page problem. The relaunch starts from a clean profile directory, so anything the old browser held — cookies, logins — is gone.
-
Two kernel mechanisms, not one. Linux confines with Landlock, seccomp and namespaces. macOS confines with Seatbelt, which is default deny across filesystem, network, mach and sysctl in one policy, and which (unlike Landlock) can subtract a child from a granted parent, so the agent config lock is one rule there instead of a bind mount. That does not make
fs.denystronger on macOS: a denied path inside a granted parent is refused as a policy on every platform, so what is left is already outside every grant. Two things are genuinely absent on macOS: there is no syscall filter, because Darwin has no seccomp equivalent; and there is no memory or process-count cap, because Darwin has no cgroups, does not enforceRLIMIT_ASagainst an mmap'd heap, and scopesRLIMIT_NPROCto the whole user rather than to one box (applying it would cap your machine, not the box).h5i box probenames the mechanism and the gaps. Rootless Podman runs on Linux and WSL2 natively, and on macOS through apodman machineVM. -
A macOS box shares the host's loopback. A Linux box gets its own network namespace, so its loopback is private. macOS has no namespaces, so a box binds the host's loopback (deliberately: it is the only way a dev server in a box is reachable). h5i closes the outbound half of this, denying the box every outbound loopback destination except its own egress proxy, but the box's own listening ports are reachable by any local process.
-
Cost. A Chrome sidecar is real RAM and CPU, even headless. Headless boxes stay first class, and the browser is opt-in per box.
-
The viewport is not a desktop. CDP screencast shows the page. Native dialogs, browser chrome and anything outside the tab are invisible.
-
A dependency on the critical path.
agent-browseris someone else's release cadence. Pinned, CLI-boundary, forkable, but not ours.
| Path | What it is |
|---|---|
.h5i/env.toml |
Checked-in policy: profiles, services, container image. |
.git/.h5i/env/<agent>/<slug>/ |
One box: its manifest, resolved policy, receipts, workspace. |
.git/.h5i/cache/<eco>/<key>/ |
Warm dependency caches. |
~/.config/h5i/ |
Host-side egress allowlist. Outside every box-granted path. |
All optional; h5i ships with working defaults.
| Variable | Purpose |
|---|---|
H5I_AGENT |
Which runtime a box is scoped to (claude, codex). Decides the agent profile's credentials and egress. |
H5I_DEFAULT_ISOLATION |
Pin this clone's default tier when --isolation is not given. --isolation auto re-probes past it. |
H5I_SECRET_<NAME> |
Default source for a secret grant <NAME>. Injected for one run, redacted from evidence, audited by fingerprint. |
H5I_SKILL_DIR |
Where h5i skill install writes. |
H5I_CREDENTIAL_PROXY |
Turn the credential proxy off (0) for a box that must reach the model API directly. |
H5I_LOG |
tracing_subscriber filter for h5i's own diagnostics, e.g. h5i_core=debug. Goes to stderr. RUST_LOG is honoured as a fallback. |
H5I_NO_PROBE_CACHE |
Re-probe host capabilities instead of reusing the cached answer. |
Read these to detect that you are in one; do not set them yourself.
| Variable | Meaning |
|---|---|
H5I_ENV_ID |
The box's id. Its presence is how the skill decides you are inside. |
H5I_ENV_POLICY_DIGEST |
The digest of the policy actually enforced. |
H5I_ENV_CAPTURE_SPOOL |
The box's only write window for staging receipt records. |
H5I_ENV_INBOX, H5I_ENV_BASE_TREE, H5I_ENV_AUDIT_CAPTURE |
Box plumbing. |
| Variable | Purpose |
|---|---|
H5I_TEST_CONTAINER |
Opt in to the real-container integration tests (pulls an image, makes a live call). |
H5I_TEST_NET |
Opt in to the supervised egress allowlist end-to-end test (needs outbound network). |
h5i <command> --help— the authoritative flag referenceman h5i— the terse CLI referenceskills/h5i/— the agent-facing skill (h5i skill show)ROADMAP.md— what is built and what is notSECURITY.md— reporting a vulnerability