← Blog

Hooks are sensors. The guard sits at main.

Gudjon Gudjonsson · Founder and CEO of OZ · 22 October 2026

Many teams now run several coding agents in one codebase, often on the same files. Git itself is fine, but the human-paced branch and merge workflow breaks at that speed. Two agents edit one file, and nobody finds out until the merge, hours later. Agent products have started to answer this with hooks. A hook lets an outside tool see each step, and speak up before the damage happens.

But every product speaks its own hook dialect, with its own events, answers and failure rules. bay reads them through one small set of moments, with one contract. This post explains what a hook is, which moments can refuse, and what bay guards.

What a hook is

A hook is a named moment in someone else's work. At that moment, an outsider sees the facts and may say go or stop. Every hook system we read has the same six parts. So does a surgeon's time out before the first cut, or a pilot's checklist before takeoff.

  • The moment: when, and in whose loop, such as before a tool runs.
  • The facts: what is about to happen, such as the tool, its input and the session.
  • The verdict: go or stop, and why.
  • The authority: who may say which word. In most products, one hook's stop stops the step.
  • The silence: what happens when the outsider crashes, runs out of time or never answers.
  • The trace: what stays behind, such as a log or a record of the step.

Every product speaks its own dialect

In October 2026, we read the hook systems of 22 coding agent products. They share a little and differ a lot.

  • Exit 2 means block in most products with command hooks.
  • Claude Code's event names are the common tongue, and several products copy them.
  • Some read Claude Code's own hook files: GitHub Copilot CLI, Devin CLI and Grok Build.
  • Most fail open. When a hook crashes or times out, the tool call goes ahead.
  • Few can fail closed. Claude Code, Cursor and goose offer a setting for it. Codex has none.
  • Ask is the least portable verdict. Codex rejects it, and Cursor does not enforce it.

The Agent Plugins standard left hooks out of its version 1.0.0, as too client-specific. So each tool that uses hooks has to translate every dialect itself.

Four kinds of moment

As recorded in October 2026, products list from one hook event to 33. By what the outsider can do there, they reduce to four kinds.

  • Gate: stop the next step before it happens, such as before a tool runs.
  • Report: learn what happened, such as at the end of a step, and add a note.
  • Span: mark a start or an end, such as of a session or a subagent.
  • State: learn that the session waits for a person, or went idle.

Only a gate can stop. A report never blocks, just as Git's own post hooks never do.

One small set of moments

bay translates each product's hook events into its own moments. Each hook entry runs bay hook <moment>, so the name says where in the work it fired. A moment names a place in the work, never a product: session.start, tool.before, turn.end. Claude Code's PostToolBatch and Codex's PostToolUse both end a step, so both call step.end. Agents and hooks, at getbay.io/docs/agents, lists each moment and each product's event for it.

bay answers with one of two verdicts. Exit 0 allows the step. Exit 2 denies it, with a reason code and a next step. An allow can carry a note, such as COVERAGE_UNKNOWN when bay could not judge a write. bay never asks a person from a hook, and never rewrites what the agent does. No model decides a verdict.

Which moments can refuse

Most of bay's moments only sense. Two can refuse.

  • `tool.before`, for a write: bay refuses a stale write, or a path that another live session holds.
  • `tool.before`, for the shell: bay refuses dangerous Git commands, such as a force push or git reset --hard.
  • `turn.end`: on Claude Code, bay asks a session once to save unsaved edits in its view before it stops.
  • Every other moment: bay records the step, and gives notices about the session's change.

On Claude Code, bay's tool.before hook fails closed: a crashed or hung hook blocks the call. On Codex, the same hook refuses, but Codex runs the tool when the hook fails. Codex also skips the hooks of a project it does not trust, without a message. So on Codex, bay's hook is a sensor. On Codex, notices after session start go to stderr, where a recorded session never showed them to the agent.

When bay cannot judge

A gate that cannot judge must not wave the step through. At tool.before, bay denies a payload it cannot read, with HOOK_INPUT_INVALID. It also denies a tool call that arrives under a moment name it does not know. So a settings file and a bay from different releases refuse, instead of letting writes pass. bay install rewrites the entries, and bay doctor names the old ones. On a machine without bay, each Claude Code entry exits 0, so a committed settings file blocks nothing there.

Hooks are sensors. The guard sits at main.

Most products let a hook fail open, and a product can skip a hook without a word. So bay's promises never rest on a hook. A hook only makes a refusal arrive sooner, in front of the agent about to cause it. The guard is bay's merge: it checks each change on the latest main, and merges only what passed. It works the same with no hook at all.

Each guard holds only under its own condition. Without hooks, an agent's own git push can still reach main around bay, unless you protect main. Security and privacy, at getbay.io/docs/security, lists what bay guards, and when.

Team rituals, moved into the work

Teams already stop and check at fixed moments: a stand-up, a review, a handover. People placed each one where its cost was bearable: daily, weekly, or at the merge. Hooks make some of them cheap enough to run at every step.

The ritualWhere it lives in bay
the stand-up: who works in which file?tool.before checks other sessions' paths, and bay status --fleet shows who works on what
the review: approve or request changesan approval check at merge, where a person runs bay approve; not a hook
the andon cord: anyone may stop the lineany deny at tool.before, and a red main that holds changes on its paths
the handover: it is yours nowbay talk, and a wake plugin that wakes the session that must act
the go or no-gobay merge: checks on the exact result, where unknown never passes
the retrothe session's work record on its machine, and the evidence in Git

Every gate that decides what reaches main is a bay command or a policy check. The hooks carry only the early half: the heads-up and the early stop.

Simple is hard

Simplicity guides every part of bay. The surface here is small: one command, bay hook <moment>, eleven moments and two verdicts. Keeping it small took many choices about what to leave out. bay leaves out ask, rewrites, model judges and a hook server of its own. bay help --json declares each product's hook events, which ones can refuse, and what each cannot see. A test holds every recorded agent payload to that contract: exits, reasons and time.

Not built yet

  • Hooks for Cursor, Grok Build, Antigravity and Gemini CLI.
  • Notices that reach a Codex agent after session start.
  • Support for products that read Claude Code's hook files, such as GitHub Copilot CLI.

Get started

Name your agent product in the [agent] table of .bay/config.toml, and merge it into main. Then run bay install, merge the hook file it writes into main once, and run bay doctor.

toml
[agent]
product = "claude"   # or "codex"

bay is free and open source under the MIT licence, with no server and no account. If you run many coding agents in one codebase, write to us at hello@getbay.io.

Gudjon Gudjonsson · Founder and CEO of OZ