老城里有一座几进的大宅院。主人每日的起居办事,自有一套雷打不动的章程:天不亮先开中门、清点门户;白天里要差人出去办事,办完了得回来回话;到了晚上主人歇手,最末落锁闭门。日子就这么一圈一圈地转。
可这宅子真正的规矩,不在主人嘴上,而在门房墙上那张主人亲手写下、早早贴好的红纸条上。纸条妙在它不写「此刻该做什么」,只写「逢着哪一刻,该由谁出来照章办事」。开中门那一刻归谁管,差人出门那一刻归谁管,差人回话那一刻又归谁管,各有各的条目。平日无事时,这些人只是候着;唯独到了那几个说定的关口,认领了这一刻的人,才一齐现身。
这天主人要打发小厮出门买样东西。小厮刚要迈出门槛——正撞上「出门」那个关口——墙上凡认领了这一刻的人,呼啦一下全围了上来,不分先后,各看各的。
有人翻了翻小厮手里的单子,觉得这趟买得不妥,当场拦下:不准去。有人说拦倒不必,但得回禀主人定夺。也有人看过觉得无妨,挥手放行。几个人的话凑到一处,自有一条铁律裁断:只要有一个喊「不准」,后头再多的「放行」也翻不过来;没人喊停、却有人要请示,那就得请示;唯独人人都说行,才算真行。还有人既没拦也没放,只把单子上「买两斤」悄悄改成「买一斤」,又往小厮怀里塞了张字条叫他照念——单子递出门时,已不全是主人原先写的那张了。
也有性子急的差事:有人接了吩咐,应一声便自顾自办去,主人并不站在门口等他回来。还有一桩最讲分寸——夜里闭门落锁那一刻,认领这关口的人只得片刻工夫照应,过了这点工夫,门一概照落不误,绝不为谁多等半息。
这宅子底下还压着一句话:这套章程,只在认了主、立了契的人家才作数。换作一处来路不明、主人尚未点头认下的宅子,墙上纵贴满红纸条,门房也一概不睬——免得有人趁主人还没看清门户,先借这墙、借这门,把外人塞进来的话当成主人的章程使了。
先猜揭晓前,先押一个猜测 · 你觉得这讲的是?
谜底 · The Concept
Lifecycle hooks — user code spliced into the harness's named moments
Capabilities · 能力扩展
中文速览 · Quick read
A lifecycle hook is user code that Claude Code runs at fixed points in its own lifecycle — the source describes hooks as "user-defined shell commands that can be executed at various points in Claude Code's lifecycle" (claude-code/src/utils/hooks.ts:1-4). They are not part of the model's reasoning; they are out-of-band commands the user registers ahead of time (conventionally in .claude/settings.json) that the harness invokes when it reaches a named moment. Those moments span the whole arc of a session: SessionStart as the session opens (claude-code/src/utils/sessionStart.ts:35-175), UserPromptSubmit as the user's text enters the loop (claude-code/src/utils/processUserInput/processUserInput.ts:182-187), PreToolUse just before a tool runs and PostToolUse just after it returns (claude-code/src/services/tools/toolHooks.ts:435-650, claude-code/src/services/tools/toolHooks.ts:39-191), Stop at the end of a turn (claude-code/src/query/stopHooks.ts:65-189), and SessionEnd as the session closes (claude-code/src/utils/hooks.ts:175-182). Whatever the event, each hook receives a common envelope built by createBaseHookInput — session_id, transcript_path, cwd, permission_mode, agent_id, agent_type (claude-code/src/utils/hooks.ts:301-328) — with event-specific fields layered on top.
When a lifecycle event fires, getMatchingHooks filters the registered hooks by event type and matcher pattern, deduplicating by command/prompt/url/if-condition and validating if-conditions for tool-specific events (claude-code/src/utils/hooks.ts:1603-1874). The surviving set is handed to executeHooks, an async generator that spawns every matched hook in parallel and yields progress messages plus aggregated results as they arrive (claude-code/src/utils/hooks.ts:1952-1977). A command hook is run by execCommandHook, which spawns bash or PowerShell depending on the hook's shell config and rewrites Windows paths to POSIX form for bash (claude-code/src/utils/hooks.ts:747-1335). The harness hands the hook its input as JSON on stdin — jsonStringify(hookInput) with a trailing newline (claude-code/src/utils/hooks.ts:1006, claude-code/src/utils/hooks.ts:1210) — and reads the hook's verdict from stdout, validated against hookJSONOutputSchema (claude-code/src/utils/hooks.ts:382-397), with exit codes carrying their own signal: 2 means a blocking error, 0 is success, 1-3 are non-blocking (claude-code/src/utils/hooks.ts:2647-2668). The parsed output flows through ProcessHookJSONOutput, which extracts the permission decision (allow/deny/ask), any updatedInput, a blockingError, additionalContext, and event-specific outputs (claude-code/src/utils/hooks.ts:489-737). A hook can opt out of blocking entirely: if its first stdout line is {"async":true,...} or its config sets async, the process is backgrounded via executeInBackground and the turn continues without waiting (claude-code/src/utils/hooks.ts:995-1030, claude-code/src/utils/hooks.ts:1112-1164). The caller drains the generator with a for-await-of loop that aggregates every hook's contribution — blockingError, message, preventContinuation, updatedInput, permissionBehavior (claude-code/src/utils/hooks.ts:2743-2929).
This is the one place where user code gets to intercept and rewrite the loop's decisions. Because all matching hooks for one event run in parallel, their verdicts must be reconciled — and the reconciliation is not "last wins" but a fixed precedence, deny > ask > allow: no hook can override a deny, and an ask overrides an allow (claude-code/src/utils/hooks.ts:2820-2847). That ordering is what turns a single denying hook into a hard gate rather than one vote among many. The interception is also generative, not just a veto: a PreToolUse hook can hand back hook-modified input that replaces what the model proposed before the tool ever runs (claude-code/src/services/tools/toolHooks.ts:435-650), and a PostToolUse hook can rewrite an MCP tool's output after the fact (claude-code/src/services/tools/toolHooks.ts:39-191). Because hooks are arbitrary code, the harness refuses to run them until the workspace is trusted: shouldSkipHookDueToTrust gates every hook in interactive mode, closing the window where an untrusted repo's settings could execute before the trust dialog is even answered (claude-code/src/utils/hooks.ts:286-296, claude-code/src/utils/hooks.ts:1993-1999, claude-code/src/utils/hooks.ts:3031-3036). The lifecycle framing forces other tradeoffs too: SessionEnd hooks get an aggressive 1500ms timeout (overridable via CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS) so a slow hook can't hold shutdown hostage (claude-code/src/utils/hooks.ts:175-182), and HTTP hooks are forbidden for SessionStart/Setup because the network round-trip would deadlock the sandbox in --print mode (claude-code/src/utils/hooks.ts:1850-1864). Plugin-supplied hooks are folded in before SessionStart/Setup via a memoized loadPluginHooks call, and a plugin that fails to load is logged and skipped rather than crashing the session (claude-code/src/utils/sessionStart.ts:56-127).
- The definition and the lifecycle envelope: claude-code/src/utils/hooks.ts:1-4;
createBaseHookInputclaude-code/src/utils/hooks.ts:301-328. - Dispatch:
getMatchingHooksclaude-code/src/utils/hooks.ts:1603-1874; theexecuteHooksgenerator claude-code/src/utils/hooks.ts:1952-1977; the aggregation loop claude-code/src/utils/hooks.ts:2743-2929. - Execution and I/O:
execCommandHookclaude-code/src/utils/hooks.ts:747-1335; stdin JSON claude-code/src/utils/hooks.ts:1006, claude-code/src/utils/hooks.ts:1210; output schema claude-code/src/utils/hooks.ts:382-397; exit-code handling claude-code/src/utils/hooks.ts:2647-2668;ProcessHookJSONOutputclaude-code/src/utils/hooks.ts:489-737; async backgrounding claude-code/src/utils/hooks.ts:995-1030, claude-code/src/utils/hooks.ts:1112-1164. - Interception semantics: permission precedence (deny > ask > allow) claude-code/src/utils/hooks.ts:2820-2847; PreToolUse input rewrite claude-code/src/services/tools/toolHooks.ts:435-650; PostToolUse output rewrite claude-code/src/services/tools/toolHooks.ts:39-191.
- Guards and per-event quirks: trust gate claude-code/src/utils/hooks.ts:286-296, claude-code/src/utils/hooks.ts:1993-1999, claude-code/src/utils/hooks.ts:3031-3036; SessionEnd timeout claude-code/src/utils/hooks.ts:175-182; HTTP forbidden for SessionStart/Setup claude-code/src/utils/hooks.ts:1850-1864; plugin loading claude-code/src/utils/sessionStart.ts:56-127; SessionStart firing claude-code/src/utils/sessionStart.ts:35-175; UserPromptSubmit claude-code/src/utils/processUserInput/processUserInput.ts:182-187; Stop hooks claude-code/src/query/stopHooks.ts:65-189.
classDiagram
class LifecycleEvent {
<<enum>>
SessionStart
UserPromptSubmit
PreToolUse
PostToolUse
Stop
SessionEnd
}
class HookEnvelope {
+session_id
+cwd permission_mode
+agent_id agent_type
}
class executeHooks {
<<parallel async generator>>
+getMatchingHooks()
+execCommandHook bashOrPwsh
+stdinJson stdoutSchema
}
class HookOutput {
+permission allow_ask_deny
+updatedInput
+blockingError
}
class Reconcile {
<<precedence>>
deny over ask over allow
}
LifecycleEvent ..> executeHooks : fires matched
executeHooks ..> HookEnvelope : stdin payload
executeHooks ..> HookOutput : parsed verdict
HookOutput ..> Reconcile : merged 读法:生命周期各点触发钩子;每个钩子收到统一信封 createBaseHookInput
(hooks.ts:301)。getMatchingHooks 过滤后 executeHooks 并行跑(:1603,1952),
stdin 喂 JSON、stdout 按 schema 解析、退出码 2=阻塞(:1006,382,2647)。多钩子裁决按
deny > ask > allow 归并(:2820):一个 deny 即硬闸。拦截亦可生成——PreToolUse 回
updatedInput 改写入参(toolHooks.ts:435)。工作区未受信任则全部跳过(hooks.ts:286)。
模型想跑一个工具:Bash · rm -rf build/。在它真正执行前,
PreToolUse 的三个钩子同时开跑,各自给一个裁决。给每个钩子选 allow / ask / deny,
让其中一个顺手改写入参,再点放行试试 —— 看最终结果不是「谁最后谁算」,而是固定优先级
deny > ask > allow:一个 deny 就是硬闸。把工作区切到未受信任,所有钩子直接跳过。
所有匹配的钩子并行执行(executeHooks 是 async generator,hooks.ts:1952-1977),裁决按 deny > ask > allow 归并——没有钩子能盖过 deny,ask 盖过 allow(hooks.ts:2820-2847)。拦截不只是否决: PreToolUse 钩子能交回改写后的 updatedInput 替换模型的提案(toolHooks.ts:435-650)。因为钩子是任意代码, 工作区未受信任时一律跳过(hooks.ts:286-296)。
What it is
A lifecycle hook is user code that Claude Code runs at fixed points in its own lifecycle — the source describes hooks as "user-defined shell commands that can be executed at various points in Claude Code's lifecycle" (claude-code/src/utils/hooks.ts:1-4). They are not part of the model's reasoning; they are out-of-band commands the user registers ahead of time (conventionally in .claude/settings.json) that the harness invokes when it reaches a named moment. Those moments span the whole arc of a session: SessionStart as the session opens (claude-code/src/utils/sessionStart.ts:35-175), UserPromptSubmit as the user's text enters the loop (claude-code/src/utils/processUserInput/processUserInput.ts:182-187), PreToolUse just before a tool runs and PostToolUse just after it returns (claude-code/src/services/tools/toolHooks.ts:435-650, claude-code/src/services/tools/toolHooks.ts:39-191), Stop at the end of a turn (claude-code/src/query/stopHooks.ts:65-189), and SessionEnd as the session closes (claude-code/src/utils/hooks.ts:175-182). Whatever the event, each hook receives a common envelope built by createBaseHookInput — session_id, transcript_path, cwd, permission_mode, agent_id, agent_type (claude-code/src/utils/hooks.ts:301-328) — with event-specific fields layered on top.
How it works
When a lifecycle event fires, getMatchingHooks filters the registered hooks by event type and matcher pattern, deduplicating by command/prompt/url/if-condition and validating if-conditions for tool-specific events (claude-code/src/utils/hooks.ts:1603-1874). The surviving set is handed to executeHooks, an async generator that spawns every matched hook in parallel and yields progress messages plus aggregated results as they arrive (claude-code/src/utils/hooks.ts:1952-1977). A command hook is run by execCommandHook, which spawns bash or PowerShell depending on the hook's shell config and rewrites Windows paths to POSIX form for bash (claude-code/src/utils/hooks.ts:747-1335). The harness hands the hook its input as JSON on stdin — jsonStringify(hookInput) with a trailing newline (claude-code/src/utils/hooks.ts:1006, claude-code/src/utils/hooks.ts:1210) — and reads the hook's verdict from stdout, validated against hookJSONOutputSchema (claude-code/src/utils/hooks.ts:382-397), with exit codes carrying their own signal: 2 means a blocking error, 0 is success, 1-3 are non-blocking (claude-code/src/utils/hooks.ts:2647-2668). The parsed output flows through ProcessHookJSONOutput, which extracts the permission decision (allow/deny/ask), any updatedInput, a blockingError, additionalContext, and event-specific outputs (claude-code/src/utils/hooks.ts:489-737). A hook can opt out of blocking entirely: if its first stdout line is {"async":true,...} or its config sets async, the process is backgrounded via executeInBackground and the turn continues without waiting (claude-code/src/utils/hooks.ts:995-1030, claude-code/src/utils/hooks.ts:1112-1164). The caller drains the generator with a for-await-of loop that aggregates every hook's contribution — blockingError, message, preventContinuation, updatedInput, permissionBehavior (claude-code/src/utils/hooks.ts:2743-2929).
Why it matters
This is the one place where user code gets to intercept and rewrite the loop's decisions. Because all matching hooks for one event run in parallel, their verdicts must be reconciled — and the reconciliation is not "last wins" but a fixed precedence, deny > ask > allow: no hook can override a deny, and an ask overrides an allow (claude-code/src/utils/hooks.ts:2820-2847). That ordering is what turns a single denying hook into a hard gate rather than one vote among many. The interception is also generative, not just a veto: a PreToolUse hook can hand back hook-modified input that replaces what the model proposed before the tool ever runs (claude-code/src/services/tools/toolHooks.ts:435-650), and a PostToolUse hook can rewrite an MCP tool's output after the fact (claude-code/src/services/tools/toolHooks.ts:39-191). Because hooks are arbitrary code, the harness refuses to run them until the workspace is trusted: shouldSkipHookDueToTrust gates every hook in interactive mode, closing the window where an untrusted repo's settings could execute before the trust dialog is even answered (claude-code/src/utils/hooks.ts:286-296, claude-code/src/utils/hooks.ts:1993-1999, claude-code/src/utils/hooks.ts:3031-3036). The lifecycle framing forces other tradeoffs too: SessionEnd hooks get an aggressive 1500ms timeout (overridable via CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS) so a slow hook can't hold shutdown hostage (claude-code/src/utils/hooks.ts:175-182), and HTTP hooks are forbidden for SessionStart/Setup because the network round-trip would deadlock the sandbox in --print mode (claude-code/src/utils/hooks.ts:1850-1864). Plugin-supplied hooks are folded in before SessionStart/Setup via a memoized loadPluginHooks call, and a plugin that fails to load is logged and skipped rather than crashing the session (claude-code/src/utils/sessionStart.ts:56-127).
Read the source
- The definition and the lifecycle envelope: claude-code/src/utils/hooks.ts:1-4;
createBaseHookInputclaude-code/src/utils/hooks.ts:301-328. - Dispatch:
getMatchingHooksclaude-code/src/utils/hooks.ts:1603-1874; theexecuteHooksgenerator claude-code/src/utils/hooks.ts:1952-1977; the aggregation loop claude-code/src/utils/hooks.ts:2743-2929. - Execution and I/O:
execCommandHookclaude-code/src/utils/hooks.ts:747-1335; stdin JSON claude-code/src/utils/hooks.ts:1006, claude-code/src/utils/hooks.ts:1210; output schema claude-code/src/utils/hooks.ts:382-397; exit-code handling claude-code/src/utils/hooks.ts:2647-2668;ProcessHookJSONOutputclaude-code/src/utils/hooks.ts:489-737; async backgrounding claude-code/src/utils/hooks.ts:995-1030, claude-code/src/utils/hooks.ts:1112-1164. - Interception semantics: permission precedence (deny > ask > allow) claude-code/src/utils/hooks.ts:2820-2847; PreToolUse input rewrite claude-code/src/services/tools/toolHooks.ts:435-650; PostToolUse output rewrite claude-code/src/services/tools/toolHooks.ts:39-191.
- Guards and per-event quirks: trust gate claude-code/src/utils/hooks.ts:286-296, claude-code/src/utils/hooks.ts:1993-1999, claude-code/src/utils/hooks.ts:3031-3036; SessionEnd timeout claude-code/src/utils/hooks.ts:175-182; HTTP forbidden for SessionStart/Setup claude-code/src/utils/hooks.ts:1850-1864; plugin loading claude-code/src/utils/sessionStart.ts:56-127; SessionStart firing claude-code/src/utils/sessionStart.ts:35-175; UserPromptSubmit claude-code/src/utils/processUserInput/processUserInput.ts:182-187; Stop hooks claude-code/src/query/stopHooks.ts:65-189.
来源 · Source citations
- [1]
claude-code/src/utils/hooks.ts:1-4 - [2]
claude-code/src/utils/hooks.ts:175-182 - [3]
claude-code/src/utils/hooks.ts:286-296 - [4]
claude-code/src/utils/hooks.ts:301-328 - [5]
claude-code/src/utils/hooks.ts:382-397 - [6]
claude-code/src/utils/hooks.ts:489-737 - [7]
claude-code/src/utils/hooks.ts:747-1335 - [8]
claude-code/src/utils/hooks.ts:995-1030 - [9]
claude-code/src/utils/hooks.ts:1006 - [10]
claude-code/src/utils/hooks.ts:1112-1164 - [11]
claude-code/src/utils/hooks.ts:1210 - [12]
claude-code/src/utils/hooks.ts:1603-1874 - [13]
claude-code/src/utils/hooks.ts:1850-1864 - [14]
claude-code/src/utils/hooks.ts:1952-1977 - [15]
claude-code/src/utils/hooks.ts:1993-1999 - [16]
claude-code/src/utils/hooks.ts:2647-2668 - [17]
claude-code/src/utils/hooks.ts:2743-2929 - [18]
claude-code/src/utils/hooks.ts:2820-2847 - [19]
claude-code/src/utils/hooks.ts:3031-3036 - [20]
claude-code/src/utils/sessionStart.ts:35-175 - [21]
claude-code/src/utils/sessionStart.ts:56-127 - [22]
claude-code/src/query/stopHooks.ts:65-189 - [23]
claude-code/src/services/tools/toolHooks.ts:39-191 - [24]
claude-code/src/services/tools/toolHooks.ts:435-650 - [25]
claude-code/src/utils/processUserInput/processUserInput.ts:182-187