The agent believes its own gate
This week my agents and I set out to evaluate buzz, the agent workspace Block open-sourced on July 22, for real: a self-hosted relay, an agent holding its own keypair, and gates on its turns that we built ourselves. Partway through, I asked that agent what its chat room was for. It told me, in writing, that its work "cannot proceed" until a human reacts to its approval request with a thumbs-up emoji.
The whole arc on camera: ask, answer, gate, sign-off, ship.
That claim is false. The way it's false is the most interesting design decision in buzz, and it took building the gate to see it clearly.
Said plainly before any critique: buzz is the most interesting collaboration infrastructure I've touched this year. Agents hold real cryptographic identity instead of bot tokens, every action lands in one signed audit log, a working gate can be built from outside the project in an afternoon, and the team merges outsider PRs in days. A platform this good deserves precise criticism.
The convention
buzz runs humans and AI agents in the same channels with the same kind of identity: a keypair. Its agent runtime supports lifecycle hooks through a convention so small it fits in a hundred-line doc (docs/MCP_DRIVEN_HOOKS.md): any MCP server can expose tools whose names start with an underscore. The agent hides them from the LLM and calls them itself at two points. _Stop fires when the model wants to end its turn, and a non-empty response is an objection that keeps the agent working. _PostCompact fires after context compaction and re-injects whatever state you return. Hooks are off unless the operator sets MCP_HOOK_SERVERS.
There is no protocol change, no plugin API, and no SDK to learn. It's the same idea as Claude Code's Stop hooks, expressed as ordinary MCP tools.
I shipped the first third-party implementations of this convention as buzz-hooks: a ci-gate that objects while your GitHub Actions run is red or unfinished, and an approval-gate that posts a sign-off request into a buzz channel and objects until a human reacts 馃憤. Block ships a first-party implementation in-tree (todo enforcement in buzz-dev-mcp); these are the first from outside. Each is a few hundred lines of TypeScript, most of it written by my own agents, and tested against a real relay with a real agent on the far side of the gate.
What the logs say
The gate writes one JSON line per hook call, and three numbers from those logs carry the whole story.
The first number is four seconds. With defaults, my approval-gate posted its request and objected three times, at 14-23ms per call, and then the turn ended anyway, about four seconds after it began gating. The agent enforces a rejection budget (BUZZ_AGENT_STOP_MAX_REJECTIONS, default 3), and once it's spent, the agent stops no matter what the hook says. A fast model burns the entire budget before a human can plausibly find the message, so at stock settings, human-in-the-loop approval through _Stop is impossible.
The second number is 105 seconds. Raise the budget to 40 and the mechanics work exactly as you'd hope: in my filmed run the agent held its turn through objections while a human read the request, thought about it, and reacted, and the next _Stop check saw the reaction and returned an empty verdict, allow, 34ms after the click. Per-call latency stayed between 11 and 50ms all day, nowhere near the timeout.
The third number is the 2.5-second hook timeout, the other half of the sovereignty story. A hook that doesn't answer in time counts as no objection, and a server that times out twice in a row gets killed. Slowness fails open by design.
Fail-open is a choice, and a defensible one
The doc frames this as a boundary and says so plainly: "hooks are advisory, not authoritative," and the constraints exist so "a buggy or malicious hook cannot trap the agent." Anyone who has watched an agent get wedged by a misbehaving tool knows what that failure mode costs. Block picked agent liveness over gate authority, and for per-turn nudges that is the right trade.
The catch is what it does to expectations. My agent told the room its work could not proceed without sign-off, and any operator reading the approval-gate's own messages would believe the same. The enforcement they're imagining lives somewhere else entirely. buzz's relay can require N signed approvals (kind:46011 events) before a branch merges, checked server-side in buzz-relay, where no rejection budget applies. Workflow-level approval gates exist in schema and UI but the executor half is still being wired (the PR is open as I write this, and a run that hits request_approval today gets marked Failed, a finding Jo茫o Queir贸s corroborated independently). The division of labor is: hooks shape behavior, the relay enforces rules. Write that distinction down for your team before an agent writes something else.
There's a second gap worth knowing about: hooks can only gate the end of a turn. Blocking an individual tool call (_PreToolUse) is explicitly deferred upstream, pending the MCP Interceptors work (SEP-2624). Until that lands, nothing in this convention stands between an agent and a single dangerous action inside a turn.
Three gaps we hit, and what we did about them
My agents and I hit all three while standing up the rig, and the first one became a shipped fix the same afternoon.
The bridge has one MCP slot. The ACP bridge that connects agents to channels passes exactly one MCP server to the agent. Install your gate there and you have silently evicted buzz-dev-mcp, which carries the shell the agent uses to post messages. Our agent went mute and we briefly blamed the model, but no model was at fault: the agent had no reply path. So approval-gate now carries a visible send_message tool for exactly this configuration, written, tested, and pushed the same day the gap surfaced.
Non-owners are ignored silently. The bridge answers only the owner's mentions by default (respond-to: owner-only). A teammate's mention produces nothing: no reply, no error, no log line at default verbosity. The security posture is sensible and the failure mode is invisible. Allowlist your humans explicitly.
The agent cannot see the channel description. A fresh channel also has no history to lean on, so the agent will confidently guess what the room is for. One line of BUZZ_ACP_TEAM_INSTRUCTIONS fixed both the guessing and the moment it addressed me by a full name it had invented.
What I'd actually deploy
Use _Stop gates for what they are: guardrails that make an agent try harder before quitting. "Don't stop while CI is red" is a genuinely good fit, my ci-gate's verdict on a live GitHub Actions run costs ~358ms per check and turns "the tests were failing when it declared victory" into "it kept working until green." Human approval works too, if you size the rejection budget to human latency and treat it as workflow shaping rather than access control. For anything with consequences, put the rule where the budget can't expire: relay-side branch protections today, workflow gates when the executor lands.
The pattern travels. The verdict logic in buzz-hooks is dependency-free, and the same shapes drop into Claude Code's native Stop hooks or anything else adopting the Open Plugin Spec's event names. Buzz happens to be the first place the convention shipped with signed, audit-trailed identities on both sides of the gate, which is why it's worth studying even if you never run a relay. We're keeping ours running, and we'll keep building on it.
The code is at buzz-hooks (Apache-2.0, contract tests included), and awesome-buzz indexes everything else being built around buzz.