- Command hooks — shell commands, configured in
agents.json. The daemon spawns the command, writes the event as JSON to stdin, and reads a JSONHookOutputfrom stdout (exit2= deny). Language-agnostic. - Atom-pack hooks — in-process typed TypeScript handlers registered by an atom pack via
the SDK (
hook({ event, matcher, handler })). The daemon itself registers a few built-in atom hooks (e.g. the memory subsystem injects recalled context onBeforeTurn).
Naming
Flat<Before|After><Subject> PascalCase with a self-evident subject (Turn, Model,
Tool, Compact, Subagent), plus SessionStart/SessionEnd lifecycle facts and
ApprovalRequest for the human approval gate.
AfterTool, AfterSubagent, and AfterTurn fire on BOTH success and failure — there is
no separate failure event; the failure is carried in the input (ok / error) and the handler
decides what to do. AfterModel is the exception: it fires only after a successful response;
a failed model call ends the turn through AfterTurn with reason: 'error'.
Source of truth in code:
packages/protocol/src/hooks.ts (contract),
apps/monad/src/hooks/runner.ts
(dispatch), packages/environment/src/config/index.ts (hooks +
policyHooks), packages/sdk-atom/src/hook.ts (SDK).
Events
Thirteen events, in firing order. “Can deny / mutate” is what a hook’sHookOutput may do at
that juncture; fields not relevant to an event are ignored.
¹
SessionStart context is stashed and injected into that session’s first BeforeTurn.
Model events — scoping
BeforeModel / AfterModel fire around the agent’s reasoning calls only — the main turn
loop and forked subagent loops. They do not fire for infra/utility model calls
(summarization → use Before/AfterCompact; embeddings, vision, tool-search, memory have no
model hook). The input’s caller distinguishes them:
BeforeSubagent to bracket a whole fork, or filters BeforeModel by
caller.kind to scope to main vs subagent reasoning. They fire per model step (every
request in a turn — including the intermediate responses that carry tool calls), not once per
turn. That is the distinction from AfterTurn: AfterModel fires after every reasoning
response; AfterTurn fires once, when the turn ends.
The contract
Configuration
Command hooks — agents.json
event → matcher[] → hooks[]. The matcher is a regex on the tool name and applies only
to the tool-scoped events (BeforeTool, AfterTool, ApprovalRequest); other events always
match.
HookInput JSON on stdin, runs with cwd = the sandbox root:
- exit 2 → deny (stderr is the reason);
- exit 0 + JSON stdout → parsed (schema-validated) as
HookOutput; empty stdout → allow; - any other failure (non-zero exit, non-JSON, spawn error, timeout) → skipped, unless
onError: "deny"(fails closed); - env is sanitized (
MONAD_*andKEY|TOKEN|SECRET|PASSWORD|CREDENTIALstripped); default timeout 60 s.
Policy hooks — policyHooks
Same shape, but operator-managed and non-overridable: they run before user hooks
and the settings API never writes them. Shell command hooks only.
Atom-pack hooks — SDK
Dispatch semantics
- Fast path — no matching hooks → returns immediately, spawns nothing.
- Order & dedup — atom hooks, then policy command hooks, then user command hooks; identical command specs run once per event.
- Serial (mutating) events chain — each hook sees the previous one’s rewrite, and the
first
denyshort-circuits. Parallel events (PARALLEL_HOOK_EVENTS) fan out. - Fail-closed — a hook’s own failure is skipped by default;
onError: 'deny'turns it into a block. - Audit seam — every executed hook is reported to
deps.record(outcome + latency). - Hot-reload —
config/policyresolved per call.
Sequence: one turn
Examples
Deny a tool via the gate (auto-deny):Notes & limits
- A
BeforeModeldeny aborts the turn via the error path (AfterTurnfires withreason: 'error'). ApprovalRequestcan auto-deny or auto-approve;ask/no-decision defers to the human gate.modelOverrideis applied only if the daemon vouches for the model.continueWorkis bounded bymaxStopContinues.- Behaviour is identical over both transports; coverage in
apps/monad/test/e2e/hooks.test.ts,apps/monad/test/unit/hooks/hooks-runner.test.ts, andapps/monad/test/unit/agent/loop-hooks.test.ts.