跪拜 Guibai
← All articles
Frontend · Agent · Artificial Intelligence

Hooks Are the Agent's Programmable Spine: 11 Lifecycle Events, 4 Execution Engines

By 樊小肆 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

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.

Summary

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.

Takeaways
11 lifecycle event types cover the agent from SessionStart to SessionEnd, plus sub-agent, compression, and permission events.
Only PreToolUse and UserPromptSubmit can deny (block) an action; all other events are observation-only and cannot stop the flow.
PreToolUse hooks can rewrite tool arguments via argsOverride (waterfall: each hook updates ctx.args in-place, subsequent hooks see the new values).
PostToolUse hooks can rewrite the model's view of a tool result via resultOverride (last-wins among concurrent hooks), while the user still sees the original output.
Four execution engines share one dispatch skeleton: command (shell, exit-code decision), http (webhook POST), prompt (text injection, no deny), agent (sub-agent review, PreToolUse only).
The command engine supports a stdout JSON protocol: scripts can output {deny, reason, argsOverride, resultOverride} to participate in blocking and rewriting without changing their exit-code convention.
The agent engine spawns a sub-agent for semantic review but is gated to depth 0 — sub-agents' own tool calls skip the hook to prevent recursive fan-out.
A master switch DEEP_SEEK_HOOK_REWRITE defaults off; when off, argsOverride and resultOverride are always undefined, reverting the system to block-only behavior.
Interceptable events run serially with short-circuit (first deny stops the chain); observation events run concurrently with Promise.all to avoid cumulative latency.
Hook failure never crashes the agent: interceptable hooks default to fail-open (allow on error), observation hooks silently swallow errors.
Timeout is tiered by latency sensitivity — 10s for SessionStart/UserPromptSubmit, 30s for tool events, 60s for SessionEnd — with a 300s hard cap.
Environment variables are whitelisted before passing to hook commands to prevent secret leakage; stdin payload fields are truncated to 4KB to avoid pipe backpressure.
HOOK_FILE_PATH is injected as an environment variable so simple one-liner scripts like prettier can work without parsing stdin JSON.
Conclusions

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.

Concepts & terms
Hook Lifecycle Event
One of 11 predefined moments in an agent's execution — SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, SessionEnd, SubagentStart/Stop, PreCompact/PostCompact, PermissionRequest — where external logic can be mounted.
Interceptable vs Observation Events
Only PreToolUse and UserPromptSubmit can deny (block) the agent's flow. All other events are observation-only — they can record or rewrite the model's view but cannot stop execution. The split ensures blocking power is only available at points where an action can still be prevented.
Hook Execution Engine
One of four backends that execute a hook rule: command (spawns a shell process), http (POSTs context to a webhook), prompt (injects text into the model's context), or agent (spawns a sub-agent for semantic review). All four expose the same HookRule.run interface to the dispatch layer.
stdout JSON Decision Protocol
A convention for command-engine hooks: when a script exits 0, it can output a JSON line on stdout with fields like deny, reason, argsOverride, or resultOverride. Non-JSON output is silently ignored, preserving backward compatibility with plain-text hooks like prettier.
Waterfall Rewrite (argsOverride)
For PreToolUse interceptable events, hooks run serially. When a hook returns argsOverride, ctx.args is updated in-place before the next hook runs, so every subsequent hook and the safety gate sees the rewritten arguments. If a later hook denies, unconsumed overrides are discarded.
Last-Wins Rewrite (resultOverride)
For PostToolUse observation events, hooks run concurrently. resultOverride values are collected from all matching hooks in registration order, and the last one wins — a deliberate trade-off to keep observation events concurrent rather than serializing them for rewrite ordering.
Agent Hook Depth Gate
A guard that prevents recursive explosion: agent-engine hooks only trigger for the main agent (depth=0). Sub-agents' tool calls (depth>0) skip agent hooks entirely. Command, HTTP, and prompt engines are not gated because they do not spawn sub-agents.
Fail-Open vs Fail-Closed
When a hook handler throws an error (not a deliberate deny), the onError field decides: 'allow' (fail-open, default) lets the action proceed, preventing a broken auxiliary hook from blocking the agent; 'deny' (fail-closed) blocks the action, used for security-critical hooks where a crash means protection has failed.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗