Hooks Are the Agent's Programmable Spine: 11 Lifecycle Events, 4 Execution Engines
Extensible agent runtimes need a disciplined interception model, not ad-hoc callbacks. The four-engine dispatch skeleton decouples execution backends from lifecycle events, so adding a new engine never touches the dispatch logic — a pattern directly portable to any agent framework that needs user-defined guardrails, audits, or context injection without forking the core loop.
An agent's lifecycle is not a black box. Session start, user input, tool calls, compression, and shutdown are all mountable nodes. DeepSeeker-Code defines 11 event types, but only two — PreToolUse and UserPromptSubmit — can deny an action; the rest are observation-only. This dichotomy is the safety core: blocking is reserved for the last moments before something irreversible happens.
Four execution engines share a single dispatch skeleton. The command engine spawns a shell and decides via exit code plus a stdout JSON protocol. HTTP POSTs the full context to a webhook and adopts the remote decision. Prompt injects text into the model's context without blocking. The agent engine spawns a full sub-agent to semantically review a tool call — powerful but expensive, and gated to depth 0 to prevent recursive explosion.
Rewrite capabilities follow the same intercept/observe boundary. PreToolUse hooks can waterfall-override arguments before execution; PostToolUse hooks can override the result the model sees, last-wins among concurrent hooks. A master switch defaults off, keeping the system in a "block-only" safe mode. The whole design rests on a fault-tolerance iron law: a crashing hook must never take down the agent.
The interceptable/observation dichotomy is not a missing feature — it is a deliberate safety boundary. Adding deny to PostToolUse would be meaningless because the file is already written; the system refuses to offer a control that cannot actually prevent the action.
The four-engine dispatch skeleton is a textbook example of the strategy pattern applied to agent extensibility. dispatch knows only HookRule.run; whether that run spawns a shell, POSTs JSON, or spawns a sub-agent is entirely the compiler's concern. Adding a fifth engine changes compileRule and nothing else.
The stdout JSON protocol is a clever backward-compatibility hack. Scripts that output plain text (prettier, linters) are silently ignored by the decision parser, so existing hooks continue working unchanged. Only scripts that explicitly emit the JSON schema gain blocking or rewriting power.
The agent engine's depth gate is a narrowly scoped fix for a narrowly scoped problem. Only agent hooks can recurse, so only agent hooks get the gate. Command and HTTP hooks still fire at all depths — a precise, minimal intervention rather than a blanket restriction.
The fail-open default for hook errors is the right call for an extensibility system. Most hooks are auxiliary (formatting, logging); a broken prettier hook should not block a file write. But delegating fail-closed to individual rules via onError lets security-critical hooks opt into stricter behavior.
Promise.resolve().then(() => run(ctx)) instead of Promise.resolve(run(ctx)) is a microtask footgun that would silently break the fault-tolerance guarantee. The fact that it is explicitly commented suggests it was learned the hard way.