Sandbox Series · Part 4 · 2026-06-12 · Updated 2026-06-26

Sandboxing AI Agents, Part 4: h5i's Tier Ladder and Policy Model

h5i's env sandbox is not one box. It is a four-rung ladder (workspace, process, supervised, container) that lets you match confinement to how much you trust the run and to what the host can actually enforce, fail-closed, with every run captured as Git provenance.

Key takeaways
  • h5i's env is a four-rung ladder (workspace, process, supervised, container), fail-closed, matching confinement to trust and to what the host can enforce.
  • The two egress allowlists are different claims: supervised is L3/L4 (nftables and slirp4netns); container is an L7 CONNECT proxy; the process tier cannot do a domain allowlist at all.
  • The whole policy is one checked-in .h5i/env.toml, digested into the manifest and captured as Git provenance.

An AI agent that can edit your tree and run shell commands is, for the duration of a task, a process executing instructions you did not write and cannot fully predict. The instructions can be redirected by a poisoned dependency, a hostile file in the repo, or a prompt-injected web page. The usual reflexes, "run it in a container" or "review the diff afterward," answer only half of the question. A single container is one fixed boundary applied to every run regardless of trust, and an after-the-fact diff review tells you what changed in the files but nothing about what the process reached for: which paths it read, which hosts it tried to call, which limits it hit. The interesting design question is not "is it in a box?" but "is the box matched to how much I trust this run, can this host actually enforce that box, and is what happened written down?"

h5i's sandbox feature is named env, and its answer to that question is a tier ladder rather than a single box. You declare how much confinement a run needs; h5i resolves that claim against the host and either enforces it or refuses; it never silently downgrades. The whole run is then captured as Git-addressed provenance, so the sandbox boundary and the review boundary become the same unit of work. This is the confinement pillar of an auditable workspace made concrete.

Series map. This h5i walkthrough follows the foundations, the implementation guide, and the AI sandbox comparison. For the end-to-end workflow around the sandbox, see the h5i env walkthrough.

What you already know, and what h5i adds

If you have sandboxed a build before, the primitives here are familiar: Linux namespaces, Landlock and seccomp, cgroups and rlimits, rootless containers, an egress proxy. h5i does not invent new kernel mechanisms. What it adds is the part that is usually left to a pile of shell scripts and tribal knowledge: a single declared policy that selects among those mechanisms, a resolver that refuses claims the host cannot meet, and a binding from the enforced policy to a reviewable record. The novelty is the discipline, not the syscalls.

The env: one Git-addressed unit of work

An h5i environment fuses three objects. The code branch is a Git worktree and branch for the agent's file changes. The context branch is the h5i reasoning and memory context for the work. The environment manifest records the policy, identity, base commit, captures, and a policy digest. Together they make the run audit-reconstructable: not bit-for-bit replay, but a durable answer to who ran what, under which policy, against which tree, producing which diff.

h5i env lifecycle
$ h5i env create fix-auth --profile agent     # pick a profile; tier resolved vs host
$ h5i env shell fix-auth                       # confined interactive session
box$ cargo test
box$ exit
$ h5i env diff fix-auth                         # inspect the filesystem diff
$ h5i env propose fix-auth                       # mediated commit + review brief
$ h5i env apply fix-auth                          # reviewer-selected; never automatic

The reviewer gets a normal shape: create, run or shell, inspect the diff, propose the mediated commit, apply when satisfied. apply refuses an unproposed environment: the mediated-commit step is mandatory, never implicit. The agent gets a workspace that feels like a real checkout. The repository gets a durable record rather than an unstructured transcript.

The tier ladder: match the box to the trust

h5i does not pretend every host supports every boundary, and it does not pretend every run needs the strongest one. There are four shipped tiers. Each rung adds enforcement and adds host requirements; each is fail-closed, so a run that asks for more than the host can deliver is refused, not weakened.

TierFilesystemNetworkEnforced byHost requirement
workspace worktree on the host FS; no kernel confinement inherits the host network Git worktree isolation only none (works anywhere Git does)
process Landlock allowlist: worktree writable, selected paths read-only, home excluded by default deny (empty netns) or host; no domain allowlist Landlock + seccomp deny-list + user/mount/IPC/UTS (and net on deny) namespaces + PID-namespace supervisor + cgroup v2 or rlimits Landlock-capable kernel, unprivileged user namespaces, seccomp
supervised same Landlock allowlist as process deny, host, or an L3/L4 domain allowlist (net.egress) the process stack plus a seccomp-notify socket gate and an nftables egress filter in a private netns process-tier requirements + rootless nftables + slirp4netns
container read-only rootfs, private /tmp tmpfs, $WORK bind-mounted rw, --userns=keep-id deny, host, or an L7 proxy domain allowlist (net.egress) rootless Podman: --cap-drop=ALL, --security-opt=no-new-privileges, no docker socket, host-side CONNECT proxy rootless Podman + a container.image in the profile

The two reserved slots, hardened-container and microvm, are external, separate-kernel backends that this build does not ship, and h5i says so plainly rather than pretending the enum value implies the boundary. An isolation enum should express both current capability and a fail-closed upgrade path.

Read the ladder by trust. workspace is for runs you already trust (your own scripted refactor) where you only want parallel-edit isolation. process is the everyday default for agent build/test work: it keeps a confused or prompt-injected command away from your home directory and the network without the weight of a container. supervised is for when the run legitimately needs the network but only to specific hosts, and you want that enforced at the packet layer. container is for runs that need a packaged toolchain or an image-level filesystem, accepting an L7 (not L3/L4) egress claim in exchange.

Ask the host first: h5i env probe

Because the ladder is host-dependent, h5i ships a probe. It does not just check that the kernel bits exist; the required Landlock/seccomp/userns features can all be present while a hardened kernel or an AppArmor profile still denies exec under the full stack. So the probe also runs a functional self-test (verify_exec) before it reports a tier as runnable.

h5i env probe
$ h5i env probe
── Host isolation capabilities ──
  os           = linux
  landlock_abi = 3
  userns       = true
  seccomp      = true
  container    = podman

  claim workspace  satisfiable = yes
  claim process    satisfiable = yes
  claim container  satisfiable = yes (needs rootless Podman + profile container.image)
  claim hardened-container/microvm: external backends (not in this build)
  process tier runnable = yes

If a requested claim is not satisfiable, env create fails with a specific reason. For example, an AppArmor-restricted user namespace on CI fails the functional check even though the Landlock ABI looks fine. Refusing is the point: a sandbox that downgrades itself silently is worse than no sandbox, because the policy digest would then assert a boundary the run never had.

The policy model: one checked-in env.toml

A profile is a small, version-controlled TOML block in .h5i/env.toml. It is the single place that selects a tier and declares the filesystem, network, resource, environment, secret, and tool surface for a run. Two built-ins need no file at all: default (a deny-home build/test profile) and the agent family (an agent-in-box profile). Everything below is fail-closed: an unset list grants nothing, and several lints reject contradictory policies before a run starts.

.h5i/env.toml
# Checked in, versioned, and digested into the env manifest.
[profile.ci]
isolation = "supervised"          # workspace | process | supervised | container
tools     = ["cargo", "rustc", "git"]   # empty = any program; non-empty = allowlist

[profile.ci.fs]
read  = ["~/.cargo", "~/.rustup"] # extra read-only grants ($WORK is writable already)
write = []                        # extra writable paths beyond $WORK
deny  = []                        # lint: a denied path may not sit under a grant

[profile.ci.net]
mode   = "deny"                   # deny (no network) | host (full rootless NAT)
egress = ["crates.io", "static.crates.io", "github.com:443"]
                                  # a non-empty allowlist supersedes `mode`: only
                                  # these hosts, enforced at L3/L4 here (supervised)

[profile.ci.resources]
mem   = "2G"
procs = 256
wall  = "10m"
cpu   = "5m"
fsize = "512M"

[profile.ci.env]
pass = ["PATH", "CARGO_HOME", "RUSTUP_HOME"]   # the rest of the host env is stripped

The fail-closed lints are where this earns its keep. h5i refuses a profile that sets net.egress below the supervised tier: the process tier's network namespace is all-or-nothing, so it can offer deny or host but cannot honor a domain allowlist, and claiming otherwise would be a lie in the manifest. It also refuses a granted filesystem path that contains a denied child, because Landlock is allowlist-only and cannot subtract a child from a granted parent, so the fix is to narrow the grant, not to pretend a deny rule will catch it.

Secrets are brokered rather than passed through. A grant names a secret and how to source and inject it (env:VAR or a host-side command: extractor; injected as a file or an env var, with an optional TTL); captures redact the value and record a fingerprint, so a reviewer can see that a secret was involved without seeing it. And the agent-in-box profiles are runtime-scoped: agent-claude grants only Claude's own HOME state and Anthropic egress, agent-codex only Codex's state and OpenAI egress. A Claude box cannot read Codex's credentials or reach the other vendor's API, which keeps a prompt-injected agent from laundering one runtime's authority through another.

Two egress allowlists, two honest claims

The network is where the tiers differ most, and where the distinction matters for your threat model. Both supervised and container can enforce a net.egress domain allowlist, but they do it at different layers and with different guarantees, and h5i is deliberate about not blurring them.

The supervised tier enforces egress at L3/L4. It runs the workload in a private network namespace with a user-space uplink (slirp4netns), installs a default-drop nftables ruleset, resolves the allowlisted hosts once and pins them through a private /etc/hosts, and opens no general DNS path. The remaining escape is policy mutation, flushing nftables or rewriting routes, so the seccomp-notify socket gate denies AF_NETLINK (along with raw and packet sockets). Tools such as nft and ip cannot even open the control channel they would need.

supervised egress behavior
curl https://crates.io           # allowed: crates.io is in net.egress
curl https://example.invalid     # blocked: not in the pinned /etc/hosts, no DNS route
curl https://1.1.1.1             # blocked: nftables default-drop at L3/L4
nft flush ruleset                # blocked: AF_NETLINK denied by the seccomp gate

The container tier enforces egress at L7. A non-empty net.egress spawns a host-side, DNS-pinned HTTP/HTTPS CONNECT allowlist proxy; the container reaches it over slirp4netns and is pointed at it with HTTP(S)_PROXY. This is honest L7 scoping: it gates proxy-respecting tooling cleanly, but it cannot stop a process that ignores the proxy environment and opens a raw socket to whatever IP the rootless NAT permits. That is a real, documented difference: a proxy allowlist and a packet-enforced allowlist are not the same claim, and the container tier does not pretend to give the L3/L4 guarantee the supervised tier does. Airtight L3/L4 inside a container is the job of the not-yet-shipped hardened/microVM tiers.

Worktree, mediated commit, and captured evidence

The worktree gives the agent a real checkout, but at the confined tiers h5i keeps the shared Git internals out of the box and treats the filesystem diff as the output. The host-side h5i process stages and commits after checking the paths, so the untrusted process never gets direct write access to refs, hooks, shared objects, or your other worktrees. That is what makes review ordinary: you can inspect a diff, compare several environments side by side, and apply the one you want. Nothing auto-merges because a command exited zero.

Every run is capture-wrapped. h5i records the command, a redacted summary, the raw output pointer, the policy digest, an egress summary, redactions, and denials. The dashboard groups this "boundary pressure" into five lanes (filesystem, network, process and privilege, resources, and provenance) with deliberately careful copy: a blocked off-allowlist request is evidence, not proof of malice, and a sensitive access under workspace isolation is a weak-isolation gap, not a red alert. The payoff is triage. A reviewer can prioritize the env that attempted raw-IP egress, touched sensitive paths, hit seccomp denials, or blew its resource budget, and spend less time on the clean ones.

The honest ceiling

All four shipped tiers share the host kernel. They are strong against accidental damage, many prompt-injection effects, ordinary untrusted build scripts, and casual exfiltration. They are not a Firecracker- or Kata-backed microVM boundary, and a malicious binary with a working host-kernel exploit is outside their honest claim. Likewise, no tier makes a careless policy safe: if a profile grants credentials and permits egress to a general-purpose endpoint, the policy itself has opened a channel. Sandboxing reduces authority; it does not replace policy design.

The hard ceiling is the shared kernel. Process, supervised, and rootless-container tiers are strong local containment tools. They are not a substitute for separate-kernel isolation when the workload itself is a determined adversary. That is what the reserved hardened-container and microVM tiers are for.

Conclusion: a ladder, not a box

The reason h5i exposes four tiers instead of one is that "trust" is not a constant across agent runs, and "what the host can enforce" is not constant across machines. A single fixed box forces you to either over-confine routine work or under-confine the run that finally reaches the open internet. A ladder lets the policy say exactly how much authority a run should have, the resolver refuse anything the host cannot deliver, and the manifest record which boundary actually applied.

That last part is what separates running an agent in a container from running agent work as an auditable environment. The container may be isolated; you still have to take its isolation on faith and reconstruct what happened from logs. An h5i env binds the enforced policy to a reviewable, comparable, shareable record, so the question "what was this process allowed to do, and what did it try?" has an answer in Git, not in your memory.

Frequently asked questions

Which tier should I use by default? For everyday agent build/test work, process is the right default where the host supports it: it excludes your home directory and the network without a container's weight. Drop to workspace only for runs you already trust; step up to supervised when the run needs specific hosts at the packet layer, or container when it needs a packaged image. Run h5i env probe to see which are satisfiable on your machine.

Can the process tier restrict the network to specific domains? No. The process tier offers only net.mode = deny (an empty network namespace) or host, because its netns is all-or-nothing. A net.egress domain allowlist requires the supervised tier (L3/L4 nftables) or the container tier (L7 proxy); h5i refuses a profile that sets net.egress below supervised rather than silently ignoring it.

Is the container tier's egress allowlist airtight? It is honest L7 scoping, not an airtight L3/L4 guarantee. The host-side CONNECT proxy gates proxy-respecting tooling, but a process that ignores HTTP(S)_PROXY and opens a raw socket can reach any IP the rootless NAT permits. For packet-level enforcement, use the supervised tier; for un-bypassable egress, the separate-kernel tiers are the (not-yet-shipped) answer.

Is an h5i env as safe as a VM? No. Every shipped tier shares the host kernel, so a working kernel exploit is outside their claim. They are designed for accidental damage, prompt-injection effects, and ordinary untrusted build scripts, not for running a determined adversary's binary. Separate-kernel isolation (Firecracker/Kata-style microVMs) is a reserved future tier.

Give the agent a box and keep the receipt

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 Read part 3