← All posts
By The agentjail team

v0.8.0: Six binaries became two

Six binaries became two: smaller install, updates that can't half-apply, and the same guardrail - with the hook still on its own fast path.

Before: six separate agentjail binaries to build, sign, notarize, and update. After: two real binaries, with the four role names as symlinks that always track the current build.

agentjail v0.8.0 is out. This is a plumbing release with a user-visible payoff: the same safety, with half as many moving parts. agentjail used to ship six separate executables; now it ships two. Your install is smaller, your updates can no longer land half-applied, and the guardrail that runs on every tool call is exactly as fast as it was.

TL;DR

  • Six binaries became two. The daemon, shield, netproxy, and secrets roles are folded into a single agentjail binary that knows which role to play from the name it was invoked as. Fewer artifacts to build, sign, notarize, and self-update.
  • Your agentjail-daemon / -shield / -netproxy / -secrets commands still work - they are now symlinks to agentjail, reconciled on every install and update, so a role can never drift out of sync with the build it should track.
  • The fast path stays fast. The hook that runs on every single tool call is deliberately kept as its own lean binary - no policy engine or database linked in - so per-call latency is unchanged.
  • We caught a release-blocking bug on the way out - after the refactor the release build’s version stamp silently stopped applying, which would have shipped this very release reporting its version as dev. Fixed before the tag.

Six binaries became two

Until now, installing agentjail dropped six executables into ~/.agentjail/bin: the agentjail CLI, the agentjail-hook that every coding agent calls, and four long-running roles - agentjail-daemon, agentjail-shield, agentjail-netproxy, and agentjail-secrets. Each one statically linked the same shared code, and each one had to be built, code-signed, notarized, and atomically swapped on its own. Six of everything.

That is six chances for something to go wrong. The most common failure was drift: an update path that refreshed some binaries but not others, leaving a stale role binary running against a newer daemon. We had already papered over one such gap; the sixth binary landing in the previous release made the seams more obvious.

v0.8.0 collapses the four service roles and the CLI into one multicall agentjail binary. A single Go program can behave as several tools depending on the name it is launched under - the classic BusyBox trick - so agentjail inspects its own invocation name and dispatches to the daemon, shield, netproxy, secrets, or CLI accordingly. There is also a plain agentjail daemon ... subcommand form as a fallback.

The result is two real binaries: agentjail and agentjail-hook. Half as much to build, sign, notarize, and update - and likely a smaller footprint on disk, since one shared image replaces five near-identical static binaries.

Plenty of things already reference the role binaries by name: the launchd and systemd service definitions that start the daemon, PATH shims, the hook configuration each coding agent reads. Breaking those names was not an option.

So the four role names still exist on disk - as relative symlinks to agentjail. When the daemon’s service manager launches ~/.agentjail/bin/agentjail-daemon, it resolves to agentjail, which sees it was invoked as agentjail-daemon and runs the daemon. Nothing that referenced a role name has to change.

The important part is how those symlinks are maintained. Every path that lays down a role name - agentjail install, agentjail update, the daemon’s background auto-update, the shell install.sh, and the Homebrew formula - now reconciles the symlinks against the freshly written agentjail binary, replacing whatever is there (including a stale real file left by a pre-upgrade install) with a fresh link. A role name is, by construction, always the current build. The whole class of “one binary updated, another didn’t” bugs is gone.

The fast path stays fast

The one binary we did not fold in is agentjail-hook.

The hook runs on every tool call your coding agent makes - every file read, every shell command, every MCP call - and it runs against a single-digit-millisecond budget. In Go, every package linked into a binary pays its initialization cost at startup, regardless of which sub-command actually runs. Folding the daemon’s policy engine and embedded database into the hook would have made every tool call pay to initialize machinery it never uses - roughly six extra milliseconds per call, on a hot path where that is the whole budget.

So the hook stays its own lean binary: no policy engine, no database, just a thin client that talks to the already-running daemon over a local socket. We verified it after the refactor - it still links none of that weight and starts in about five milliseconds. Consolidation buys simpler releases without spending a microsecond of the latency budget that makes agentjail usable in the first place.

The bug we caught on the way out

One story from this release is worth telling because it is exactly the kind of thing that ships silently.

agentjail stamps its version into the binary at build time. Before the refactor, each role binary was its own main package and the release build injected the version into main.version. After the refactor, those roles became imported packages rather than main - and a version stamp aimed at main no longer reaches an imported package. The build does not error; the stamp just quietly does nothing.

We had fixed this for the local build during the refactor, but the CI release pipeline still used the old form. Left unnoticed, this release would have shipped reporting its version as dev - agentjail version would have lied, and so would every crash report and telemetry event. We caught it in the pre-release audit and moved the whole project onto a single shared version symbol that every binary reads and one fully-qualified build flag sets. agentjail version tells the truth again.

Also in this release

  • Cleaner coding-agent hook configuration. agentjail no longer writes a degenerate empty Codex hook group or a null Cursor event entry when it reconciles an agent’s hooks.json, both of which could produce malformed config that the agent would warn about on startup.

Upgrading

Nothing changes in how you use agentjail. curl-based installs, brew upgrade, and the built-in self-update all produce the new two-binary layout with the role symlinks in place; existing service definitions and hook configuration keep working untouched. If you have scripts that invoke agentjail-daemon, agentjail-shield, agentjail-netproxy, or agentjail-secrets by name, they continue to work - those names now resolve to the one agentjail binary.