v1.0.0
agentjail 1.0 — see what your coding agent sends its model, on Linux and macOS.
The most revealing thing an agent does is talk to its model: the prompt it assembled, the tools it was handed, the slice of your codebase it pulled in, and what came back. On Linux, agentjail has captured that traffic through its transparent tunnel + MITM for releases. v1.0.0 brings the same full visibility to macOS — the harder platform, with no system extension required — and levels up the tunnel on both with HTTP/2, gRPC, and IPv6. All verified against a real agent on the build you actually install.
Highlights
- Full network capture on macOS, no system extension. Linux already captured your agent's traffic through the tunnel; on macOS a transparent MITM can't decrypt
POST /v1/messagesbecause Bun's inference client ignores every CA. So v1.0.0 points the agent's ownANTHROPIC_BASE_URLat a local, egress-guarded capture gateway that records the full request/response and forwards to the real API over TLS — no privileged extension needed (AGE-259, ADR 0109). - The transparent tunnel, now with HTTP/2, gRPC, and IPv6. The same gVisor + MITM engine on Linux and macOS now negotiates ALPN and serves
h2with full parity to HTTP/1.1, carries gRPC status and trailers, keeps streaming RPCs from stalling, and captures IPv6 egress instead of leaking it (ADR 0102/0108, AGE-262). - Proven on the installed build. A live Claude Code session decrypted and captured end to end behind a golden-VM gate, closing three defects only the shipped binary + a real agent could hit (ADR 0103).
- A network-visibility UI. Per-session request timelines, column filters, and a body viewer over an on-disk capture store.
- One canonical network config (ADR 0110): CLI flag beats env beats config beats default, and
agentjail doctorprints each knob's effective value and where it came from.
Upgrading: nothing changes in how you run agentjail — curl installs, brew upgrade, and self-update pick up 1.0 automatically, and existing config and policy keep working. Read the full story on the blog.
Added
- Base-URL capture gateway for LLM providers (AGE-259, ADR 0109): current Claude Code runs on Bun and its inference client ignores every CA trust store, so a transparent MITM cannot decrypt
POST /v1/messages(and in fact breaks it). Instead the shield points the agent's ownANTHROPIC_BASE_URLat a per-session loopback proxy — nonce-gated, egress-guarded, a user-set base URL is preserved — that records the full request/response (bodies encrypted at rest) and forwards to the real provider over TLS. Runs in both plain and--tunnelmodes, independent of the system extension; fail-closed for a detected provider with an explicit--no-provider-gateway/network.capture_gateway: falseopt-out. The darwin non-tunnel path became spawn-and-wait (fromsyscall.Exec) so the in-process gateway survives, preserving exit-code and signal parity. - IPv6 datapath for the macOS tunnel (AGE-262, opt-in): IPv6 egress was grabbed by the extension but had no v6 datapath, so it reset (
SSL_ERROR_SYSCALL) and evaded capture while IPv4 worked. The gateway now provisions a v6 address (fd79::, outside the DNS-VIP pool) end to end; enable with--tunnel-ipv6/network.tunnel_ipv6(default off until the installed app is attested dual-stack). - Network flag precedence +
doctorsourcing (ADR 0110): one canonical order for every network knob — CLI flag > env var >policy.yaml> default — andagentjail doctornow prints each knob's effective value AND where it came from (cli/env/config/default).--netproxyis documented as deprecated. - HTTP/2 in the tunnel MITM (ADR 0102): advertises
h2, http/1.1, serves h2 viahttp2.Server.ServeConnwith anhttp2.Transportupstream, full parity with the HTTP/1.1 path (body capture, policy/deny, recording). gRPC unary works withgrpc-statusthrough trailers; hop-by-hop headers are stripped on both legs. Proven end-to-end over the real TUN. - Streaming/bidi h2 request bodies are forwarded without pre-draining, so a long-lived stream never deadlocks; host/path/method policy still applies (body-content scan is bounded-body only — ADR 0102).
- Encrypted body capture with keychain/file KEK tiers so
doctornever overstates protection (ADR 0095/0097). agentjail ui --trusted-host: reach the local UI behind a same-host reverse proxy without disabling the anti-rebinding guard (ADR 0099).agentjail-daemon --retention-interval: retention + WAL checkpoint re-run periodically, not just at startup (ADR 0101).- Network session "active" state reflects the owning shield PID's liveness, matching
agentjail sessions list --active(ADR 0100).
Fixed
- Streaming responses stalled on the macOS tunnel (ADR 0108): the gVisor
serverNetstackpump dropped outbound packets when its queue was full, silently losing TCP segments on a lossless carrier and hanging long SSE streams. It now applies backpressure (blocks, never drops). - Decrypted bodies were not persisted on the darwin tunnel (AGE-259): the session id was
shield-<ts>-<hex>, whichmitm.NewBodyStorerejected (non-alnum), silently disabling body storage. The id is now alphanumeric, so request/response bodies actually land. - Dev builds (
make dev-deploy/dev-install) reported versiondev— the-ldflagstargeted a nonexistentmain.versioninstead ofinternal/buildinfo.Version(AGE-247). - Retention enforced its window only once at daemon startup, so a long-lived daemon's DB grew unbounded (AGE-225).
- h2 request trailers were dropped on streamed bodies (
.Clone()froze an all-nil map before net/http2 filled it). - Tunnel silently fell back to netproxy on every installed deployment (ADR 0103): the TUN-holder /
nsenterre-exec usedos.Executable(), which resolves the installedagentjail-shieldsymlink toagentjailand misdispatched to the CLI, so the TUN helper never ran. It worked only from the standalone dev binary, hiding the bug. Now dispatched byargv[0]. - Tunnel holder died mid-session (ADR 0103):
Pdeathsigfires on the cloning thread's exit, which Go could retire, SIGKILLing the holder so a follow-onnsenterfailed. Replaced with socket-based liveness (the holder exits when the shield closes the handoff socket). - Agent could not resolve DNS inside the tunnel on systemd-resolved hosts (ADR 0103): the
127.0.0.53stub is unreachable in the netns, so the agent sawENOTIMP. The holder now bind-mounts a resolver pointing at the in-tunnel gateway.
Docs
- Document the unprivileged-userns requirement: Ubuntu 23.10+ gates it behind AppArmor; the tunnel fails open to netproxy when it is unavailable, and
agentjail doctorprints the one-time command to enable the full tunnel.