From 797e1b6ea4691c2e4f4f1c738ec26fad8ccdba06 Mon Sep 17 00:00:00 2001 From: Yao Lu Date: Wed, 27 May 2026 22:50:40 +0800 Subject: [PATCH] =?UTF-8?q?[S8/YAO-155]=20=E9=99=84=E5=BD=95=20C=20?= =?UTF-8?q?=E8=A1=A5=E9=BD=90=2027=20=E4=B8=AA=20Hook=20=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E7=9A=84=20trigger/payload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 scripts/data/hooks-trigger-map.json 手工库,覆盖全部 27 个 HOOK_EVENTS 的触发时机、payload 字段、schema_source、dispatch_source、call_sites, source_commit 锁定 290fdc9481a70612bc5823aa4ed225c52c52aad3。 - 改造 scripts/gen-hooks-table.ts:合并手工库渲染 4 列表格 (事件名/触发时机/Payload 字段/源码位置),并新增漂移检测——当 source_commit 不一致或 HOOK_EVENTS 与手工库出现增删时打印 warning, 让 CI gen:appendix drift 闸把住后续源码升级。 - 重生成 docs/appendix/C.md 与 manifest,TODO 占位全部清零 (已补齐 trigger/payload:27/27)。 Co-authored-by: multica-agent --- docs/appendix/C.manifest.json | 352 ++++++++++++++++++++++-- docs/appendix/C.md | 59 ++-- scripts/data/hooks-trigger-map.json | 402 ++++++++++++++++++++++++++++ scripts/gen-hooks-table.ts | 115 +++++++- 4 files changed, 860 insertions(+), 68 deletions(-) create mode 100644 scripts/data/hooks-trigger-map.json diff --git a/docs/appendix/C.manifest.json b/docs/appendix/C.manifest.json index 5a9e823..7a1dded 100644 --- a/docs/appendix/C.manifest.json +++ b/docs/appendix/C.manifest.json @@ -6,7 +6,22 @@ "category": "event", "wire_type": "PreToolUse", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:414-423", + "utils/hooks.ts:3394-3436", + "services/tools/toolHooks.ts (executePreToolHooks)" + ], + "trigger": "工具被调度执行之前;可用 exit 2 / decision:'block' 阻断本次工具调用,hook 输出会作为 system reminder 反馈给模型。", + "payload": [ + "session_id", + "transcript_path", + "cwd", + "permission_mode?", + "agent_id?", + "agent_type?", + "hook_event_name='PreToolUse'", + "tool_name", + "tool_input", + "tool_use_id" ] }, { @@ -14,7 +29,18 @@ "category": "event", "wire_type": "PostToolUse", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:436-446", + "utils/hooks.ts:3450-3477", + "services/tools/toolHooks.ts:56" + ], + "trigger": "工具调用成功完成之后;hook 可通过 decision:'block' 给模型注入反馈(不影响工具结果本身)。", + "payload": [ + "...BaseHookInput", + "hook_event_name='PostToolUse'", + "tool_name", + "tool_input", + "tool_response", + "tool_use_id" ] }, { @@ -22,7 +48,19 @@ "category": "event", "wire_type": "PostToolUseFailure", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:448-459", + "utils/hooks.ts:3492-3527", + "services/tools/toolHooks.ts:212" + ], + "trigger": "工具调用失败 / 被中断之后;payload 携带 error 字符串与 is_interrupt 标记,hook 可向模型注入修复建议。", + "payload": [ + "...BaseHookInput", + "hook_event_name='PostToolUseFailure'", + "tool_name", + "tool_input", + "tool_use_id", + "error", + "is_interrupt?" ] }, { @@ -30,7 +68,22 @@ "category": "event", "wire_type": "Notification", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:473-482", + "utils/hooks.ts:3570-3592", + "services/notifier.ts:25", + "cli/print.ts:1366", + "services/mcp/elicitationHandler.ts:183", + "services/mcp/elicitationHandler.ts:283", + "services/mcp/elicitationHandler.ts:298", + "services/mcp/elicitationHandler.ts:307" + ], + "trigger": "需要向用户弹出系统通知前(待权限确认 / 待 elicitation / 输入空闲提醒等);hook 可替换或抑制通知。在 REPL 之外执行。", + "payload": [ + "...BaseHookInput", + "hook_event_name='Notification'", + "message", + "title?", + "notification_type" ] }, { @@ -38,7 +91,15 @@ "category": "event", "wire_type": "UserPromptSubmit", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:484-491", + "utils/hooks.ts:3826-3855", + "utils/processUserInput/processUserInput.ts:182" + ], + "trigger": "用户在 REPL 中提交 prompt 之后、消息入队 QueryEngine 之前;hook 可改写 prompt 或注入额外 system 消息。", + "payload": [ + "...BaseHookInput", + "hook_event_name='UserPromptSubmit'", + "prompt" ] }, { @@ -46,7 +107,17 @@ "category": "event", "wire_type": "SessionStart", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:493-502", + "utils/hooks.ts:3867-3892", + "utils/sessionStart.ts:132" + ], + "trigger": "会话开始或 resume/clear/compact 后重启 session;hook 可注入 system reminder(如自定义 agent 引导语)。", + "payload": [ + "...BaseHookInput", + "hook_event_name='SessionStart'", + "source='startup'|'resume'|'clear'|'compact'", + "agent_type?", + "model?" ] }, { @@ -54,7 +125,17 @@ "category": "event", "wire_type": "SessionEnd", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:758-765", + "utils/hooks.ts:4097-4141", + "utils/gracefulShutdown.ts:473", + "screens/REPL.tsx:1774", + "commands/clear/conversation.ts:69" + ], + "trigger": "会话结束(logout / clear / resume 切换 / prompt_input_exit 等);同步触发,关停 Ink 之后直接写 stderr。默认 1.5s 超时(CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS 覆盖)。", + "payload": [ + "...BaseHookInput", + "hook_event_name='SessionEnd'", + "reason ∈ EXIT_REASONS" ] }, { @@ -62,7 +143,16 @@ "category": "event", "wire_type": "Stop", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:513-527", + "utils/hooks.ts:3639-3697", + "query/stopHooks.ts:180" + ], + "trigger": "主线程在一轮回合结束、准备进入 idle 之前;hook 可 exit 2 / decision:'block' 让模型继续执行,用于强制完成额外步骤。", + "payload": [ + "...BaseHookInput", + "hook_event_name='Stop'", + "stop_hook_active", + "last_assistant_message?" ] }, { @@ -70,7 +160,19 @@ "category": "event", "wire_type": "StopFailure", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:529-538", + "utils/hooks.ts:3594-3627", + "query.ts:1174", + "query.ts:1181", + "query.ts:1263" + ], + "trigger": "回合以 assistant 错误(API 错误 / 取消 / overload)收尾时,与 Stop 互斥;payload 携带最后一条 assistant message 与错误字段,主要用于审计 / 告警。", + "payload": [ + "...BaseHookInput", + "hook_event_name='StopFailure'", + "error (SDKAssistantMessageError, 缺省 'unknown')", + "error_details?", + "last_assistant_message?" ] }, { @@ -78,7 +180,16 @@ "category": "event", "wire_type": "SubagentStart", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:540-548", + "utils/hooks.ts:3932-3952", + "tools/AgentTool/runAgent.ts:532" + ], + "trigger": "AgentTool 创建并启动一个 subagent 之前;payload 给出 agent_id + agent_type,便于 hook 给子 agent 注入额外 system reminder。", + "payload": [ + "...BaseHookInput", + "hook_event_name='SubagentStart'", + "agent_id", + "agent_type" ] }, { @@ -86,7 +197,19 @@ "category": "event", "wire_type": "SubagentStop", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:550-567", + "utils/hooks.ts:3639-3697", + "query/stopHooks.ts:180" + ], + "trigger": "subagent 完成回合、准备返回结果给父 agent 之前;Stop 的 subagent 变体,executeStopHooks 在传入 subagentId 时切到此事件。可阻断让子 agent 继续工作。", + "payload": [ + "...BaseHookInput", + "hook_event_name='SubagentStop'", + "stop_hook_active", + "agent_id", + "agent_transcript_path", + "agent_type", + "last_assistant_message?" ] }, { @@ -94,7 +217,18 @@ "category": "event", "wire_type": "PreCompact", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:569-577", + "utils/hooks.ts:3961-4025", + "services/compact/compact.ts:413", + "services/compact/compact.ts:818", + "commands/compact/compact.ts:160" + ], + "trigger": "压缩流程开始之前(手动 /compact 或 token 自动压缩);hook 可返回 newCustomInstructions 覆盖默认指令,并把 displayMessage 显示给用户。", + "payload": [ + "...BaseHookInput", + "hook_event_name='PreCompact'", + "trigger='manual'|'auto'", + "custom_instructions: string|null" ] }, { @@ -102,7 +236,17 @@ "category": "event", "wire_type": "PostCompact", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:579-589", + "utils/hooks.ts:4034-4089", + "services/compact/compact.ts:723", + "services/compact/compact.ts:1069" + ], + "trigger": "压缩完成、summary 已写入会话之后;hook 输出仅作为 displayMessage 展示给用户,不参与后续模型上下文。", + "payload": [ + "...BaseHookInput", + "hook_event_name='PostCompact'", + "trigger='manual'|'auto'", + "compact_summary" ] }, { @@ -110,7 +254,19 @@ "category": "event", "wire_type": "PermissionRequest", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:425-434", + "utils/hooks.ts:4157-4192", + "utils/permissions/permissions.ts:409", + "cli/structuredIO.ts:798", + "hooks/toolPermission/PermissionContext.ts:222" + ], + "trigger": "权限引擎判定需要弹出权限对话框之前;hook 可程序化批准 / 拒绝,免去交互。在 REPL(含 SDK structuredIO 通道)与 PermissionContext 内都会触发。", + "payload": [ + "...BaseHookInput", + "hook_event_name='PermissionRequest'", + "tool_name", + "tool_input", + "permission_suggestions?: PermissionUpdate[]" ] }, { @@ -118,7 +274,18 @@ "category": "event", "wire_type": "PermissionDenied", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:461-471", + "utils/hooks.ts:3529-3562", + "services/tools/toolExecution.ts:1081" + ], + "trigger": "权限引擎判定一次工具调用被拒绝之后,先于错误结果返回模型;fire-and-forget,主要用于审计 / 告警。", + "payload": [ + "...BaseHookInput", + "hook_event_name='PermissionDenied'", + "tool_name", + "tool_input", + "tool_use_id", + "reason" ] }, { @@ -126,7 +293,15 @@ "category": "event", "wire_type": "Setup", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:504-511", + "utils/hooks.ts:3902-3922", + "utils/sessionStart.ts:203" + ], + "trigger": "会话初始化 / 周期性维护阶段,紧随 SessionStart 之后;payload trigger 区分 'init' 与 'maintenance'。", + "payload": [ + "...BaseHookInput", + "hook_event_name='Setup'", + "trigger='init'|'maintenance'" ] }, { @@ -134,7 +309,16 @@ "category": "event", "wire_type": "TeammateIdle", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:591-599", + "utils/hooks.ts:3709-3729", + "query/stopHooks.ts:403" + ], + "trigger": "teammate(多人协作中的另一个 agent)即将进入 idle 状态时;hook 可 exit 2 让其继续工作,用于强制完成轮询任务。", + "payload": [ + "...BaseHookInput", + "hook_event_name='TeammateIdle'", + "teammate_name", + "team_name" ] }, { @@ -142,7 +326,19 @@ "category": "event", "wire_type": "TaskCreated", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:601-612", + "utils/hooks.ts:3745-3773", + "tools/TaskCreateTool/TaskCreateTool.ts:93" + ], + "trigger": "TaskCreateTool 创建任务前;hook 可阻断创建并向模型回馈拒绝理由(如校验任务标题命名规范)。", + "payload": [ + "...BaseHookInput", + "hook_event_name='TaskCreated'", + "task_id", + "task_subject", + "task_description?", + "teammate_name?", + "team_name?" ] }, { @@ -150,7 +346,19 @@ "category": "event", "wire_type": "TaskCompleted", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:614-625", + "utils/hooks.ts:3789-3817", + "tools/TaskUpdateTool/TaskUpdateTool.ts:235" + ], + "trigger": "TaskUpdateTool 将任务置为 completed 之前;hook 可阻断完成(例如强制要求附录 / 验收脚本通过)。", + "payload": [ + "...BaseHookInput", + "hook_event_name='TaskCompleted'", + "task_id", + "task_subject", + "task_description?", + "teammate_name?", + "team_name?" ] }, { @@ -158,7 +366,20 @@ "category": "event", "wire_type": "Elicitation", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:627-643", + "utils/hooks.ts:4470-4523", + "services/mcp/elicitationHandler.ts:228" + ], + "trigger": "MCP 服务器发起 elicitation 请求、需要向用户弹出对话框前;hook 可程序化返回 accept/decline/cancel 与表单内容,跳过 UI。", + "payload": [ + "...BaseHookInput", + "hook_event_name='Elicitation'", + "mcp_server_name", + "message", + "mode='form'|'url'?", + "url?", + "elicitation_id?", + "requested_schema?" ] }, { @@ -166,7 +387,19 @@ "category": "event", "wire_type": "ElicitationResult", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:645-660", + "utils/hooks.ts:4525-4575", + "services/mcp/elicitationHandler.ts:273" + ], + "trigger": "用户已经响应 MCP elicitation、即将把结果发回服务器之前;hook 可观察或改写最终响应(含 decline 转 block)。", + "payload": [ + "...BaseHookInput", + "hook_event_name='ElicitationResult'", + "mcp_server_name", + "elicitation_id?", + "mode='form'|'url'?", + "action='accept'|'decline'|'cancel'", + "content?" ] }, { @@ -174,7 +407,18 @@ "category": "event", "wire_type": "ConfigChange", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:670-678", + "utils/hooks.ts:4214-4239", + "utils/settings/changeDetector.ts:292", + "utils/settings/changeDetector.ts:344", + "utils/skills/skillChangeDetector.ts:267" + ], + "trigger": "配置文件 watcher 检测到 user/project/local/policy settings 或 skills 目录变化时;hook 可 exit 2 阻断本次变更生效(policy_settings 例外,hook 仅审计、不可阻断)。", + "payload": [ + "...BaseHookInput", + "hook_event_name='ConfigChange'", + "source ∈ CONFIG_CHANGE_SOURCES", + "file_path?" ] }, { @@ -182,7 +426,17 @@ "category": "event", "wire_type": "WorktreeCreate", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:709-716", + "utils/hooks.ts:4928-4958", + "utils/worktree.ts:716", + "utils/worktree.ts:913", + "utils/worktree.ts:1262" + ], + "trigger": "需要为本会话创建 worktree 时;若用户配置了该 hook,则替代默认 git worktree 流程,stdout 中的路径作为新 worktree 根目录。无配置时回退到 git worktree。", + "payload": [ + "...BaseHookInput", + "hook_event_name='WorktreeCreate'", + "name (slug,已经过 validateWorktreeSlug)" ] }, { @@ -190,7 +444,16 @@ "category": "event", "wire_type": "WorktreeRemove", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:718-725", + "utils/hooks.ts:4967-5003", + "utils/worktree.ts:827", + "utils/worktree.ts:968" + ], + "trigger": "会话结束 / 用户主动清理 worktree 时;若 hook 配置存在则替代默认 git worktree remove;返回 false 表示无 hook 配置、走默认路径。", + "payload": [ + "...BaseHookInput", + "hook_event_name='WorktreeRemove'", + "worktree_path" ] }, { @@ -198,7 +461,21 @@ "category": "event", "wire_type": "InstructionsLoaded", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:695-707", + "utils/hooks.ts:4335-4369", + "utils/claudemd.ts:1060", + "utils/attachments.ts:1760" + ], + "trigger": "CLAUDE.md 或 .claude/rules/*.md 等指令文件被加载进上下文时(session_start / nested_traversal / path_glob_match / include / compact 五种场景);fire-and-forget,仅用于观察与审计,不能阻断。", + "payload": [ + "...BaseHookInput", + "hook_event_name='InstructionsLoaded'", + "file_path", + "memory_type ∈ {User, Project, Local, Managed}", + "load_reason ∈ INSTRUCTIONS_LOAD_REASONS", + "globs?", + "trigger_file_path?", + "parent_file_path?" ] }, { @@ -206,7 +483,16 @@ "category": "event", "wire_type": "CwdChanged", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:727-735", + "utils/hooks.ts:4260-4276", + "utils/hooks/fileChangedWatcher.ts:148" + ], + "trigger": "会话 cwd 发生切换之后(如 /cd 命令、worktree 切换);hook 输出可声明 watchPaths 让 FileChanged watcher 重新订阅,并向 REPL 推 systemMessage。", + "payload": [ + "...BaseHookInput", + "hook_event_name='CwdChanged'", + "old_cwd", + "new_cwd" ] }, { @@ -214,7 +500,16 @@ "category": "event", "wire_type": "FileChanged", "source_files": [ - "entrypoints/sdk/coreSchemas.ts:355-383" + "entrypoints/sdk/coreSchemas.ts:737-745", + "utils/hooks.ts:4278-4294", + "utils/hooks/fileChangedWatcher.ts:85" + ], + "trigger": "FileChangedWatcher 监听到 watchPaths 内文件 add/change/unlink 之后;hook 可输出新的 watchPaths 动态扩缩订阅集,输出失败将作为系统消息提示用户。", + "payload": [ + "...BaseHookInput", + "hook_event_name='FileChanged'", + "file_path", + "event='change'|'add'|'unlink'" ] }, { @@ -248,6 +543,7 @@ ], "counts": { "events": 27, - "command_types": 4 + "command_types": 4, + "events_with_trigger": 27 } } diff --git a/docs/appendix/C.md b/docs/appendix/C.md index a2a3985..131190a 100644 --- a/docs/appendix/C.md +++ b/docs/appendix/C.md @@ -4,38 +4,39 @@ - HOOK_EVENTS:27 个 - Hook command type:4 类 +- 已补齐 trigger/payload:27/27 ## HOOK_EVENTS(来源:`entrypoints/sdk/coreSchemas.ts:355-383`) -| 事件名 | -|---| -| `PreToolUse` | -| `PostToolUse` | -| `PostToolUseFailure` | -| `Notification` | -| `UserPromptSubmit` | -| `SessionStart` | -| `SessionEnd` | -| `Stop` | -| `StopFailure` | -| `SubagentStart` | -| `SubagentStop` | -| `PreCompact` | -| `PostCompact` | -| `PermissionRequest` | -| `PermissionDenied` | -| `Setup` | -| `TeammateIdle` | -| `TaskCreated` | -| `TaskCompleted` | -| `Elicitation` | -| `ElicitationResult` | -| `ConfigChange` | -| `WorktreeCreate` | -| `WorktreeRemove` | -| `InstructionsLoaded` | -| `CwdChanged` | -| `FileChanged` | +| 事件名 | 触发时机 | Payload 字段 | 源码位置 | +|---|---|---|---| +| `PreToolUse` | 工具被调度执行之前;可用 exit 2 / decision:'block' 阻断本次工具调用,hook 输出会作为 system reminder 反馈给模型。 | `session_id`
`transcript_path`
`cwd`
`permission_mode?`
`agent_id?`
`agent_type?`
`hook_event_name='PreToolUse'`
`tool_name`
`tool_input`
`tool_use_id` | schema: `entrypoints/sdk/coreSchemas.ts:414-423`
dispatch: `utils/hooks.ts:3394-3436`
call: `services/tools/toolHooks.ts (executePreToolHooks)` | +| `PostToolUse` | 工具调用成功完成之后;hook 可通过 decision:'block' 给模型注入反馈(不影响工具结果本身)。 | `...BaseHookInput`
`hook_event_name='PostToolUse'`
`tool_name`
`tool_input`
`tool_response`
`tool_use_id` | schema: `entrypoints/sdk/coreSchemas.ts:436-446`
dispatch: `utils/hooks.ts:3450-3477`
call: `services/tools/toolHooks.ts:56` | +| `PostToolUseFailure` | 工具调用失败 / 被中断之后;payload 携带 error 字符串与 is_interrupt 标记,hook 可向模型注入修复建议。 | `...BaseHookInput`
`hook_event_name='PostToolUseFailure'`
`tool_name`
`tool_input`
`tool_use_id`
`error`
`is_interrupt?` | schema: `entrypoints/sdk/coreSchemas.ts:448-459`
dispatch: `utils/hooks.ts:3492-3527`
call: `services/tools/toolHooks.ts:212` | +| `Notification` | 需要向用户弹出系统通知前(待权限确认 / 待 elicitation / 输入空闲提醒等);hook 可替换或抑制通知。在 REPL 之外执行。 | `...BaseHookInput`
`hook_event_name='Notification'`
`message`
`title?`
`notification_type` | schema: `entrypoints/sdk/coreSchemas.ts:473-482`
dispatch: `utils/hooks.ts:3570-3592`
call: `services/notifier.ts:25`
call: `cli/print.ts:1366`
call: `services/mcp/elicitationHandler.ts:183`
call: `services/mcp/elicitationHandler.ts:283`
call: `services/mcp/elicitationHandler.ts:298`
call: `services/mcp/elicitationHandler.ts:307` | +| `UserPromptSubmit` | 用户在 REPL 中提交 prompt 之后、消息入队 QueryEngine 之前;hook 可改写 prompt 或注入额外 system 消息。 | `...BaseHookInput`
`hook_event_name='UserPromptSubmit'`
`prompt` | schema: `entrypoints/sdk/coreSchemas.ts:484-491`
dispatch: `utils/hooks.ts:3826-3855`
call: `utils/processUserInput/processUserInput.ts:182` | +| `SessionStart` | 会话开始或 resume/clear/compact 后重启 session;hook 可注入 system reminder(如自定义 agent 引导语)。 | `...BaseHookInput`
`hook_event_name='SessionStart'`
`source='startup'\|'resume'\|'clear'\|'compact'`
`agent_type?`
`model?` | schema: `entrypoints/sdk/coreSchemas.ts:493-502`
dispatch: `utils/hooks.ts:3867-3892`
call: `utils/sessionStart.ts:132` | +| `SessionEnd` | 会话结束(logout / clear / resume 切换 / prompt_input_exit 等);同步触发,关停 Ink 之后直接写 stderr。默认 1.5s 超时(CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS 覆盖)。 | `...BaseHookInput`
`hook_event_name='SessionEnd'`
`reason ∈ EXIT_REASONS` | schema: `entrypoints/sdk/coreSchemas.ts:758-765`
dispatch: `utils/hooks.ts:4097-4141`
call: `utils/gracefulShutdown.ts:473`
call: `screens/REPL.tsx:1774`
call: `commands/clear/conversation.ts:69` | +| `Stop` | 主线程在一轮回合结束、准备进入 idle 之前;hook 可 exit 2 / decision:'block' 让模型继续执行,用于强制完成额外步骤。 | `...BaseHookInput`
`hook_event_name='Stop'`
`stop_hook_active`
`last_assistant_message?` | schema: `entrypoints/sdk/coreSchemas.ts:513-527`
dispatch: `utils/hooks.ts:3639-3697`
call: `query/stopHooks.ts:180` | +| `StopFailure` | 回合以 assistant 错误(API 错误 / 取消 / overload)收尾时,与 Stop 互斥;payload 携带最后一条 assistant message 与错误字段,主要用于审计 / 告警。 | `...BaseHookInput`
`hook_event_name='StopFailure'`
`error (SDKAssistantMessageError, 缺省 'unknown')`
`error_details?`
`last_assistant_message?` | schema: `entrypoints/sdk/coreSchemas.ts:529-538`
dispatch: `utils/hooks.ts:3594-3627`
call: `query.ts:1174`
call: `query.ts:1181`
call: `query.ts:1263` | +| `SubagentStart` | AgentTool 创建并启动一个 subagent 之前;payload 给出 agent_id + agent_type,便于 hook 给子 agent 注入额外 system reminder。 | `...BaseHookInput`
`hook_event_name='SubagentStart'`
`agent_id`
`agent_type` | schema: `entrypoints/sdk/coreSchemas.ts:540-548`
dispatch: `utils/hooks.ts:3932-3952`
call: `tools/AgentTool/runAgent.ts:532` | +| `SubagentStop` | subagent 完成回合、准备返回结果给父 agent 之前;Stop 的 subagent 变体,executeStopHooks 在传入 subagentId 时切到此事件。可阻断让子 agent 继续工作。 | `...BaseHookInput`
`hook_event_name='SubagentStop'`
`stop_hook_active`
`agent_id`
`agent_transcript_path`
`agent_type`
`last_assistant_message?` | schema: `entrypoints/sdk/coreSchemas.ts:550-567`
dispatch: `utils/hooks.ts:3639-3697`
call: `query/stopHooks.ts:180` | +| `PreCompact` | 压缩流程开始之前(手动 /compact 或 token 自动压缩);hook 可返回 newCustomInstructions 覆盖默认指令,并把 displayMessage 显示给用户。 | `...BaseHookInput`
`hook_event_name='PreCompact'`
`trigger='manual'\|'auto'`
`custom_instructions: string\|null` | schema: `entrypoints/sdk/coreSchemas.ts:569-577`
dispatch: `utils/hooks.ts:3961-4025`
call: `services/compact/compact.ts:413`
call: `services/compact/compact.ts:818`
call: `commands/compact/compact.ts:160` | +| `PostCompact` | 压缩完成、summary 已写入会话之后;hook 输出仅作为 displayMessage 展示给用户,不参与后续模型上下文。 | `...BaseHookInput`
`hook_event_name='PostCompact'`
`trigger='manual'\|'auto'`
`compact_summary` | schema: `entrypoints/sdk/coreSchemas.ts:579-589`
dispatch: `utils/hooks.ts:4034-4089`
call: `services/compact/compact.ts:723`
call: `services/compact/compact.ts:1069` | +| `PermissionRequest` | 权限引擎判定需要弹出权限对话框之前;hook 可程序化批准 / 拒绝,免去交互。在 REPL(含 SDK structuredIO 通道)与 PermissionContext 内都会触发。 | `...BaseHookInput`
`hook_event_name='PermissionRequest'`
`tool_name`
`tool_input`
`permission_suggestions?: PermissionUpdate[]` | schema: `entrypoints/sdk/coreSchemas.ts:425-434`
dispatch: `utils/hooks.ts:4157-4192`
call: `utils/permissions/permissions.ts:409`
call: `cli/structuredIO.ts:798`
call: `hooks/toolPermission/PermissionContext.ts:222` | +| `PermissionDenied` | 权限引擎判定一次工具调用被拒绝之后,先于错误结果返回模型;fire-and-forget,主要用于审计 / 告警。 | `...BaseHookInput`
`hook_event_name='PermissionDenied'`
`tool_name`
`tool_input`
`tool_use_id`
`reason` | schema: `entrypoints/sdk/coreSchemas.ts:461-471`
dispatch: `utils/hooks.ts:3529-3562`
call: `services/tools/toolExecution.ts:1081` | +| `Setup` | 会话初始化 / 周期性维护阶段,紧随 SessionStart 之后;payload trigger 区分 'init' 与 'maintenance'。 | `...BaseHookInput`
`hook_event_name='Setup'`
`trigger='init'\|'maintenance'` | schema: `entrypoints/sdk/coreSchemas.ts:504-511`
dispatch: `utils/hooks.ts:3902-3922`
call: `utils/sessionStart.ts:203` | +| `TeammateIdle` | teammate(多人协作中的另一个 agent)即将进入 idle 状态时;hook 可 exit 2 让其继续工作,用于强制完成轮询任务。 | `...BaseHookInput`
`hook_event_name='TeammateIdle'`
`teammate_name`
`team_name` | schema: `entrypoints/sdk/coreSchemas.ts:591-599`
dispatch: `utils/hooks.ts:3709-3729`
call: `query/stopHooks.ts:403` | +| `TaskCreated` | TaskCreateTool 创建任务前;hook 可阻断创建并向模型回馈拒绝理由(如校验任务标题命名规范)。 | `...BaseHookInput`
`hook_event_name='TaskCreated'`
`task_id`
`task_subject`
`task_description?`
`teammate_name?`
`team_name?` | schema: `entrypoints/sdk/coreSchemas.ts:601-612`
dispatch: `utils/hooks.ts:3745-3773`
call: `tools/TaskCreateTool/TaskCreateTool.ts:93` | +| `TaskCompleted` | TaskUpdateTool 将任务置为 completed 之前;hook 可阻断完成(例如强制要求附录 / 验收脚本通过)。 | `...BaseHookInput`
`hook_event_name='TaskCompleted'`
`task_id`
`task_subject`
`task_description?`
`teammate_name?`
`team_name?` | schema: `entrypoints/sdk/coreSchemas.ts:614-625`
dispatch: `utils/hooks.ts:3789-3817`
call: `tools/TaskUpdateTool/TaskUpdateTool.ts:235` | +| `Elicitation` | MCP 服务器发起 elicitation 请求、需要向用户弹出对话框前;hook 可程序化返回 accept/decline/cancel 与表单内容,跳过 UI。 | `...BaseHookInput`
`hook_event_name='Elicitation'`
`mcp_server_name`
`message`
`mode='form'\|'url'?`
`url?`
`elicitation_id?`
`requested_schema?` | schema: `entrypoints/sdk/coreSchemas.ts:627-643`
dispatch: `utils/hooks.ts:4470-4523`
call: `services/mcp/elicitationHandler.ts:228` | +| `ElicitationResult` | 用户已经响应 MCP elicitation、即将把结果发回服务器之前;hook 可观察或改写最终响应(含 decline 转 block)。 | `...BaseHookInput`
`hook_event_name='ElicitationResult'`
`mcp_server_name`
`elicitation_id?`
`mode='form'\|'url'?`
`action='accept'\|'decline'\|'cancel'`
`content?` | schema: `entrypoints/sdk/coreSchemas.ts:645-660`
dispatch: `utils/hooks.ts:4525-4575`
call: `services/mcp/elicitationHandler.ts:273` | +| `ConfigChange` | 配置文件 watcher 检测到 user/project/local/policy settings 或 skills 目录变化时;hook 可 exit 2 阻断本次变更生效(policy_settings 例外,hook 仅审计、不可阻断)。 | `...BaseHookInput`
`hook_event_name='ConfigChange'`
`source ∈ CONFIG_CHANGE_SOURCES`
`file_path?` | schema: `entrypoints/sdk/coreSchemas.ts:670-678`
dispatch: `utils/hooks.ts:4214-4239`
call: `utils/settings/changeDetector.ts:292`
call: `utils/settings/changeDetector.ts:344`
call: `utils/skills/skillChangeDetector.ts:267` | +| `WorktreeCreate` | 需要为本会话创建 worktree 时;若用户配置了该 hook,则替代默认 git worktree 流程,stdout 中的路径作为新 worktree 根目录。无配置时回退到 git worktree。 | `...BaseHookInput`
`hook_event_name='WorktreeCreate'`
`name (slug,已经过 validateWorktreeSlug)` | schema: `entrypoints/sdk/coreSchemas.ts:709-716`
dispatch: `utils/hooks.ts:4928-4958`
call: `utils/worktree.ts:716`
call: `utils/worktree.ts:913`
call: `utils/worktree.ts:1262` | +| `WorktreeRemove` | 会话结束 / 用户主动清理 worktree 时;若 hook 配置存在则替代默认 git worktree remove;返回 false 表示无 hook 配置、走默认路径。 | `...BaseHookInput`
`hook_event_name='WorktreeRemove'`
`worktree_path` | schema: `entrypoints/sdk/coreSchemas.ts:718-725`
dispatch: `utils/hooks.ts:4967-5003`
call: `utils/worktree.ts:827`
call: `utils/worktree.ts:968` | +| `InstructionsLoaded` | CLAUDE.md 或 .claude/rules/*.md 等指令文件被加载进上下文时(session_start / nested_traversal / path_glob_match / include / compact 五种场景);fire-and-forget,仅用于观察与审计,不能阻断。 | `...BaseHookInput`
`hook_event_name='InstructionsLoaded'`
`file_path`
`memory_type ∈ {User, Project, Local, Managed}`
`load_reason ∈ INSTRUCTIONS_LOAD_REASONS`
`globs?`
`trigger_file_path?`
`parent_file_path?` | schema: `entrypoints/sdk/coreSchemas.ts:695-707`
dispatch: `utils/hooks.ts:4335-4369`
call: `utils/claudemd.ts:1060`
call: `utils/attachments.ts:1760` | +| `CwdChanged` | 会话 cwd 发生切换之后(如 /cd 命令、worktree 切换);hook 输出可声明 watchPaths 让 FileChanged watcher 重新订阅,并向 REPL 推 systemMessage。 | `...BaseHookInput`
`hook_event_name='CwdChanged'`
`old_cwd`
`new_cwd` | schema: `entrypoints/sdk/coreSchemas.ts:727-735`
dispatch: `utils/hooks.ts:4260-4276`
call: `utils/hooks/fileChangedWatcher.ts:148` | +| `FileChanged` | FileChangedWatcher 监听到 watchPaths 内文件 add/change/unlink 之后;hook 可输出新的 watchPaths 动态扩缩订阅集,输出失败将作为系统消息提示用户。 | `...BaseHookInput`
`hook_event_name='FileChanged'`
`file_path`
`event='change'\|'add'\|'unlink'` | schema: `entrypoints/sdk/coreSchemas.ts:737-745`
dispatch: `utils/hooks.ts:4278-4294`
call: `utils/hooks/fileChangedWatcher.ts:85` | ## Hook command type(来源:`schemas/hooks.ts`) diff --git a/scripts/data/hooks-trigger-map.json b/scripts/data/hooks-trigger-map.json new file mode 100644 index 0000000..3cdda99 --- /dev/null +++ b/scripts/data/hooks-trigger-map.json @@ -0,0 +1,402 @@ +{ + "_comment": "附录 C · Hooks 事件触发时机与 payload 字段手工库。schema_source 指 entrypoints/sdk/coreSchemas.ts;dispatch_source 指 utils/hooks.ts 内 execute*Hooks 函数;call_sites 是把上述 dispatch 函数串到产品流程的调用点。所有 file:line 范围基于 source_commit = 290fdc9481a70612bc5823aa4ed225c52c52aad3。", + "source_commit": "290fdc9481a70612bc5823aa4ed225c52c52aad3", + "events": { + "PreToolUse": { + "trigger": "工具被调度执行之前;可用 exit 2 / decision:'block' 阻断本次工具调用,hook 输出会作为 system reminder 反馈给模型。", + "payload": [ + "session_id", + "transcript_path", + "cwd", + "permission_mode?", + "agent_id?", + "agent_type?", + "hook_event_name='PreToolUse'", + "tool_name", + "tool_input", + "tool_use_id" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:414-423", + "dispatch_source": "utils/hooks.ts:3394-3436", + "call_sites": ["services/tools/toolHooks.ts (executePreToolHooks)"] + }, + "PostToolUse": { + "trigger": "工具调用成功完成之后;hook 可通过 decision:'block' 给模型注入反馈(不影响工具结果本身)。", + "payload": [ + "...BaseHookInput", + "hook_event_name='PostToolUse'", + "tool_name", + "tool_input", + "tool_response", + "tool_use_id" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:436-446", + "dispatch_source": "utils/hooks.ts:3450-3477", + "call_sites": ["services/tools/toolHooks.ts:56"] + }, + "PostToolUseFailure": { + "trigger": "工具调用失败 / 被中断之后;payload 携带 error 字符串与 is_interrupt 标记,hook 可向模型注入修复建议。", + "payload": [ + "...BaseHookInput", + "hook_event_name='PostToolUseFailure'", + "tool_name", + "tool_input", + "tool_use_id", + "error", + "is_interrupt?" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:448-459", + "dispatch_source": "utils/hooks.ts:3492-3527", + "call_sites": ["services/tools/toolHooks.ts:212"] + }, + "PermissionDenied": { + "trigger": "权限引擎判定一次工具调用被拒绝之后,先于错误结果返回模型;fire-and-forget,主要用于审计 / 告警。", + "payload": [ + "...BaseHookInput", + "hook_event_name='PermissionDenied'", + "tool_name", + "tool_input", + "tool_use_id", + "reason" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:461-471", + "dispatch_source": "utils/hooks.ts:3529-3562", + "call_sites": ["services/tools/toolExecution.ts:1081"] + }, + "Notification": { + "trigger": "需要向用户弹出系统通知前(待权限确认 / 待 elicitation / 输入空闲提醒等);hook 可替换或抑制通知。在 REPL 之外执行。", + "payload": [ + "...BaseHookInput", + "hook_event_name='Notification'", + "message", + "title?", + "notification_type" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:473-482", + "dispatch_source": "utils/hooks.ts:3570-3592", + "call_sites": [ + "services/notifier.ts:25", + "cli/print.ts:1366", + "services/mcp/elicitationHandler.ts:183", + "services/mcp/elicitationHandler.ts:283", + "services/mcp/elicitationHandler.ts:298", + "services/mcp/elicitationHandler.ts:307" + ] + }, + "UserPromptSubmit": { + "trigger": "用户在 REPL 中提交 prompt 之后、消息入队 QueryEngine 之前;hook 可改写 prompt 或注入额外 system 消息。", + "payload": [ + "...BaseHookInput", + "hook_event_name='UserPromptSubmit'", + "prompt" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:484-491", + "dispatch_source": "utils/hooks.ts:3826-3855", + "call_sites": ["utils/processUserInput/processUserInput.ts:182"] + }, + "SessionStart": { + "trigger": "会话开始或 resume/clear/compact 后重启 session;hook 可注入 system reminder(如自定义 agent 引导语)。", + "payload": [ + "...BaseHookInput", + "hook_event_name='SessionStart'", + "source='startup'|'resume'|'clear'|'compact'", + "agent_type?", + "model?" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:493-502", + "dispatch_source": "utils/hooks.ts:3867-3892", + "call_sites": ["utils/sessionStart.ts:132"] + }, + "SessionEnd": { + "trigger": "会话结束(logout / clear / resume 切换 / prompt_input_exit 等);同步触发,关停 Ink 之后直接写 stderr。默认 1.5s 超时(CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS 覆盖)。", + "payload": [ + "...BaseHookInput", + "hook_event_name='SessionEnd'", + "reason ∈ EXIT_REASONS" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:758-765", + "dispatch_source": "utils/hooks.ts:4097-4141", + "call_sites": [ + "utils/gracefulShutdown.ts:473", + "screens/REPL.tsx:1774", + "commands/clear/conversation.ts:69" + ] + }, + "Stop": { + "trigger": "主线程在一轮回合结束、准备进入 idle 之前;hook 可 exit 2 / decision:'block' 让模型继续执行,用于强制完成额外步骤。", + "payload": [ + "...BaseHookInput", + "hook_event_name='Stop'", + "stop_hook_active", + "last_assistant_message?" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:513-527", + "dispatch_source": "utils/hooks.ts:3639-3697", + "call_sites": ["query/stopHooks.ts:180"] + }, + "StopFailure": { + "trigger": "回合以 assistant 错误(API 错误 / 取消 / overload)收尾时,与 Stop 互斥;payload 携带最后一条 assistant message 与错误字段,主要用于审计 / 告警。", + "payload": [ + "...BaseHookInput", + "hook_event_name='StopFailure'", + "error (SDKAssistantMessageError, 缺省 'unknown')", + "error_details?", + "last_assistant_message?" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:529-538", + "dispatch_source": "utils/hooks.ts:3594-3627", + "call_sites": [ + "query.ts:1174", + "query.ts:1181", + "query.ts:1263" + ] + }, + "SubagentStart": { + "trigger": "AgentTool 创建并启动一个 subagent 之前;payload 给出 agent_id + agent_type,便于 hook 给子 agent 注入额外 system reminder。", + "payload": [ + "...BaseHookInput", + "hook_event_name='SubagentStart'", + "agent_id", + "agent_type" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:540-548", + "dispatch_source": "utils/hooks.ts:3932-3952", + "call_sites": ["tools/AgentTool/runAgent.ts:532"] + }, + "SubagentStop": { + "trigger": "subagent 完成回合、准备返回结果给父 agent 之前;Stop 的 subagent 变体,executeStopHooks 在传入 subagentId 时切到此事件。可阻断让子 agent 继续工作。", + "payload": [ + "...BaseHookInput", + "hook_event_name='SubagentStop'", + "stop_hook_active", + "agent_id", + "agent_transcript_path", + "agent_type", + "last_assistant_message?" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:550-567", + "dispatch_source": "utils/hooks.ts:3639-3697", + "call_sites": ["query/stopHooks.ts:180"] + }, + "PreCompact": { + "trigger": "压缩流程开始之前(手动 /compact 或 token 自动压缩);hook 可返回 newCustomInstructions 覆盖默认指令,并把 displayMessage 显示给用户。", + "payload": [ + "...BaseHookInput", + "hook_event_name='PreCompact'", + "trigger='manual'|'auto'", + "custom_instructions: string|null" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:569-577", + "dispatch_source": "utils/hooks.ts:3961-4025", + "call_sites": [ + "services/compact/compact.ts:413", + "services/compact/compact.ts:818", + "commands/compact/compact.ts:160" + ] + }, + "PostCompact": { + "trigger": "压缩完成、summary 已写入会话之后;hook 输出仅作为 displayMessage 展示给用户,不参与后续模型上下文。", + "payload": [ + "...BaseHookInput", + "hook_event_name='PostCompact'", + "trigger='manual'|'auto'", + "compact_summary" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:579-589", + "dispatch_source": "utils/hooks.ts:4034-4089", + "call_sites": [ + "services/compact/compact.ts:723", + "services/compact/compact.ts:1069" + ] + }, + "PermissionRequest": { + "trigger": "权限引擎判定需要弹出权限对话框之前;hook 可程序化批准 / 拒绝,免去交互。在 REPL(含 SDK structuredIO 通道)与 PermissionContext 内都会触发。", + "payload": [ + "...BaseHookInput", + "hook_event_name='PermissionRequest'", + "tool_name", + "tool_input", + "permission_suggestions?: PermissionUpdate[]" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:425-434", + "dispatch_source": "utils/hooks.ts:4157-4192", + "call_sites": [ + "utils/permissions/permissions.ts:409", + "cli/structuredIO.ts:798", + "hooks/toolPermission/PermissionContext.ts:222" + ] + }, + "Setup": { + "trigger": "会话初始化 / 周期性维护阶段,紧随 SessionStart 之后;payload trigger 区分 'init' 与 'maintenance'。", + "payload": [ + "...BaseHookInput", + "hook_event_name='Setup'", + "trigger='init'|'maintenance'" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:504-511", + "dispatch_source": "utils/hooks.ts:3902-3922", + "call_sites": ["utils/sessionStart.ts:203"] + }, + "TeammateIdle": { + "trigger": "teammate(多人协作中的另一个 agent)即将进入 idle 状态时;hook 可 exit 2 让其继续工作,用于强制完成轮询任务。", + "payload": [ + "...BaseHookInput", + "hook_event_name='TeammateIdle'", + "teammate_name", + "team_name" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:591-599", + "dispatch_source": "utils/hooks.ts:3709-3729", + "call_sites": ["query/stopHooks.ts:403"] + }, + "TaskCreated": { + "trigger": "TaskCreateTool 创建任务前;hook 可阻断创建并向模型回馈拒绝理由(如校验任务标题命名规范)。", + "payload": [ + "...BaseHookInput", + "hook_event_name='TaskCreated'", + "task_id", + "task_subject", + "task_description?", + "teammate_name?", + "team_name?" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:601-612", + "dispatch_source": "utils/hooks.ts:3745-3773", + "call_sites": ["tools/TaskCreateTool/TaskCreateTool.ts:93"] + }, + "TaskCompleted": { + "trigger": "TaskUpdateTool 将任务置为 completed 之前;hook 可阻断完成(例如强制要求附录 / 验收脚本通过)。", + "payload": [ + "...BaseHookInput", + "hook_event_name='TaskCompleted'", + "task_id", + "task_subject", + "task_description?", + "teammate_name?", + "team_name?" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:614-625", + "dispatch_source": "utils/hooks.ts:3789-3817", + "call_sites": ["tools/TaskUpdateTool/TaskUpdateTool.ts:235"] + }, + "Elicitation": { + "trigger": "MCP 服务器发起 elicitation 请求、需要向用户弹出对话框前;hook 可程序化返回 accept/decline/cancel 与表单内容,跳过 UI。", + "payload": [ + "...BaseHookInput", + "hook_event_name='Elicitation'", + "mcp_server_name", + "message", + "mode='form'|'url'?", + "url?", + "elicitation_id?", + "requested_schema?" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:627-643", + "dispatch_source": "utils/hooks.ts:4470-4523", + "call_sites": ["services/mcp/elicitationHandler.ts:228"] + }, + "ElicitationResult": { + "trigger": "用户已经响应 MCP elicitation、即将把结果发回服务器之前;hook 可观察或改写最终响应(含 decline 转 block)。", + "payload": [ + "...BaseHookInput", + "hook_event_name='ElicitationResult'", + "mcp_server_name", + "elicitation_id?", + "mode='form'|'url'?", + "action='accept'|'decline'|'cancel'", + "content?" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:645-660", + "dispatch_source": "utils/hooks.ts:4525-4575", + "call_sites": ["services/mcp/elicitationHandler.ts:273"] + }, + "ConfigChange": { + "trigger": "配置文件 watcher 检测到 user/project/local/policy settings 或 skills 目录变化时;hook 可 exit 2 阻断本次变更生效(policy_settings 例外,hook 仅审计、不可阻断)。", + "payload": [ + "...BaseHookInput", + "hook_event_name='ConfigChange'", + "source ∈ CONFIG_CHANGE_SOURCES", + "file_path?" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:670-678", + "dispatch_source": "utils/hooks.ts:4214-4239", + "call_sites": [ + "utils/settings/changeDetector.ts:292", + "utils/settings/changeDetector.ts:344", + "utils/skills/skillChangeDetector.ts:267" + ] + }, + "WorktreeCreate": { + "trigger": "需要为本会话创建 worktree 时;若用户配置了该 hook,则替代默认 git worktree 流程,stdout 中的路径作为新 worktree 根目录。无配置时回退到 git worktree。", + "payload": [ + "...BaseHookInput", + "hook_event_name='WorktreeCreate'", + "name (slug,已经过 validateWorktreeSlug)" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:709-716", + "dispatch_source": "utils/hooks.ts:4928-4958", + "call_sites": [ + "utils/worktree.ts:716", + "utils/worktree.ts:913", + "utils/worktree.ts:1262" + ] + }, + "WorktreeRemove": { + "trigger": "会话结束 / 用户主动清理 worktree 时;若 hook 配置存在则替代默认 git worktree remove;返回 false 表示无 hook 配置、走默认路径。", + "payload": [ + "...BaseHookInput", + "hook_event_name='WorktreeRemove'", + "worktree_path" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:718-725", + "dispatch_source": "utils/hooks.ts:4967-5003", + "call_sites": [ + "utils/worktree.ts:827", + "utils/worktree.ts:968" + ] + }, + "InstructionsLoaded": { + "trigger": "CLAUDE.md 或 .claude/rules/*.md 等指令文件被加载进上下文时(session_start / nested_traversal / path_glob_match / include / compact 五种场景);fire-and-forget,仅用于观察与审计,不能阻断。", + "payload": [ + "...BaseHookInput", + "hook_event_name='InstructionsLoaded'", + "file_path", + "memory_type ∈ {User, Project, Local, Managed}", + "load_reason ∈ INSTRUCTIONS_LOAD_REASONS", + "globs?", + "trigger_file_path?", + "parent_file_path?" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:695-707", + "dispatch_source": "utils/hooks.ts:4335-4369", + "call_sites": [ + "utils/claudemd.ts:1060", + "utils/attachments.ts:1760" + ] + }, + "CwdChanged": { + "trigger": "会话 cwd 发生切换之后(如 /cd 命令、worktree 切换);hook 输出可声明 watchPaths 让 FileChanged watcher 重新订阅,并向 REPL 推 systemMessage。", + "payload": [ + "...BaseHookInput", + "hook_event_name='CwdChanged'", + "old_cwd", + "new_cwd" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:727-735", + "dispatch_source": "utils/hooks.ts:4260-4276", + "call_sites": ["utils/hooks/fileChangedWatcher.ts:148"] + }, + "FileChanged": { + "trigger": "FileChangedWatcher 监听到 watchPaths 内文件 add/change/unlink 之后;hook 可输出新的 watchPaths 动态扩缩订阅集,输出失败将作为系统消息提示用户。", + "payload": [ + "...BaseHookInput", + "hook_event_name='FileChanged'", + "file_path", + "event='change'|'add'|'unlink'" + ], + "schema_source": "entrypoints/sdk/coreSchemas.ts:737-745", + "dispatch_source": "utils/hooks.ts:4278-4294", + "call_sites": ["utils/hooks/fileChangedWatcher.ts:85"] + } + } +} diff --git a/scripts/gen-hooks-table.ts b/scripts/gen-hooks-table.ts index c30e42d..1604b2d 100644 --- a/scripts/gen-hooks-table.ts +++ b/scripts/gen-hooks-table.ts @@ -4,11 +4,12 @@ * * - 解析 entrypoints/sdk/coreSchemas.ts 中的 `HOOK_EVENTS` 数组(27 个事件)。 * - 解析 schemas/hooks.ts 中四种 hook command type(command / prompt / http / agent)。 + * - 合并 scripts/data/hooks-trigger-map.json 手工库的触发时机 + payload 字段。 * * 用法: * bun scripts/gen-hooks-table.ts [--source-path ] [--diff-summary] */ -import { readFileSync } from "node:fs"; +import { readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; import { parseArgs, @@ -27,6 +28,20 @@ const sourceCommit = getSourceCommit(sourcePath); const coreSchemasPath = join(sourcePath, "entrypoints/sdk/coreSchemas.ts"); const hooksSchemaPath = join(sourcePath, "schemas/hooks.ts"); +const triggerMapPath = "scripts/data/hooks-trigger-map.json"; + +type TriggerEntry = { + trigger: string; + payload: string[]; + schema_source: string; + dispatch_source: string; + call_sites: string[]; +}; + +type TriggerMap = { + source_commit: string; + events: Record; +}; function extractHookEvents(text: string): { events: string[]; start: number; end: number } { const m = text.match(/export const HOOK_EVENTS\s*=\s*\[([\s\S]*?)\]\s*as\s+const/); @@ -53,20 +68,55 @@ function extractHookCommandTypes(text: string): { name: string; line: number }[] return out.sort((a, b) => a.name.localeCompare(b.name)); } +function loadTriggerMap(): TriggerMap { + if (!existsSync(triggerMapPath)) { + throw new Error( + `Missing trigger map at ${triggerMapPath}; needed to render trigger/payload columns.`, + ); + } + return JSON.parse(readFileSync(triggerMapPath, "utf8")) as TriggerMap; +} + const coreText = readFileSync(coreSchemasPath, "utf8"); const hooksText = readFileSync(hooksSchemaPath, "utf8"); const eventInfo = extractHookEvents(coreText); const cmdTypes = extractHookCommandTypes(hooksText); +const triggerMap = loadTriggerMap(); + +// Drift gate: warn loudly when source_commit or event set diverges. Do not +// silently render stale data — leaving a TODO in the rendered table is safer +// than pretending the manual map matches the bundled source. +const driftWarnings: string[] = []; +if (triggerMap.source_commit !== sourceCommit) { + driftWarnings.push( + `trigger-map source_commit ${triggerMap.source_commit} != current ${sourceCommit}; verify map is still accurate`, + ); +} +const missingFromMap = eventInfo.events.filter((e) => !triggerMap.events[e]); +const extraInMap = Object.keys(triggerMap.events).filter( + (e) => !eventInfo.events.includes(e), +); +if (missingFromMap.length > 0) { + driftWarnings.push(`events missing from trigger map: ${missingFromMap.join(", ")}`); +} +if (extraInMap.length > 0) { + driftWarnings.push(`events in trigger map but not in HOOK_EVENTS: ${extraInMap.join(", ")}`); +} const items: ManifestItem[] = [ - ...eventInfo.events.map( - (e): ManifestItem => ({ + ...eventInfo.events.map((e): ManifestItem => { + const entry = triggerMap.events[e]; + return { name: e, category: "event", wire_type: e, - source_files: [`entrypoints/sdk/coreSchemas.ts:${eventInfo.start}-${eventInfo.end}`], - }), - ), + source_files: entry + ? [entry.schema_source, entry.dispatch_source, ...entry.call_sites] + : [`entrypoints/sdk/coreSchemas.ts:${eventInfo.start}-${eventInfo.end}`], + trigger: entry?.trigger ?? "TODO: 待补 trigger", + payload: entry?.payload ?? ["TODO: 待补 payload"], + }; + }), ...cmdTypes.map( (t): ManifestItem => ({ name: t.name, @@ -79,13 +129,40 @@ const items: ManifestItem[] = [ const manifest = { source_commit: sourceCommit, items, - counts: { events: eventInfo.events.length, command_types: cmdTypes.length }, + counts: { + events: eventInfo.events.length, + command_types: cmdTypes.length, + events_with_trigger: eventInfo.events.filter((e) => triggerMap.events[e]).length, + }, }; const manifestPath = "docs/appendix/C.manifest.json"; const prev = readManifest(manifestPath); writeManifest(manifestPath, manifest); +function renderPayloadCell(payload: string[]): string { + // Escape pipes for markdown table cells and join with
so each field + // sits on its own visual line without breaking the row. + return payload + .map((p) => p.replace(/\|/g, "\\|")) + .map((p) => `\`${p}\``) + .join("
"); +} + +function renderTriggerCell(trigger: string): string { + return trigger.replace(/\|/g, "\\|").replace(/\n/g, " "); +} + +function renderSourcesCell(entry?: TriggerEntry): string { + if (!entry) return "—"; + const lines = [ + `schema: \`${entry.schema_source}\``, + `dispatch: \`${entry.dispatch_source}\``, + ...entry.call_sites.map((s) => `call: \`${s}\``), + ]; + return lines.join("
"); +} + const md = [ `# 附录 C · Hooks 事件表`, ``, @@ -93,12 +170,25 @@ const md = [ ``, `- HOOK_EVENTS:${eventInfo.events.length} 个`, `- Hook command type:${cmdTypes.length} 类`, + `- 已补齐 trigger/payload:${manifest.counts.events_with_trigger}/${eventInfo.events.length}`, ``, + ...(driftWarnings.length > 0 + ? [ + `> ⚠️ 触发表与源码可能存在漂移:`, + ...driftWarnings.map((w) => `> - ${w}`), + ``, + ] + : []), `## HOOK_EVENTS(来源:\`entrypoints/sdk/coreSchemas.ts:${eventInfo.start}-${eventInfo.end}\`)`, ``, - `| 事件名 |`, - `|---|`, - ...eventInfo.events.map((e) => `| \`${e}\` |`), + `| 事件名 | 触发时机 | Payload 字段 | 源码位置 |`, + `|---|---|---|---|`, + ...eventInfo.events.map((e) => { + const entry = triggerMap.events[e]; + const trigger = entry ? renderTriggerCell(entry.trigger) : "TODO: 待补 trigger"; + const payload = entry ? renderPayloadCell(entry.payload) : "TODO: 待补 payload"; + return `| \`${e}\` | ${trigger} | ${payload} | ${renderSourcesCell(entry)} |`; + }), ``, `## Hook command type(来源:\`schemas/hooks.ts\`)`, ``, @@ -110,6 +200,9 @@ const md = [ writeFile("docs/appendix/C.md", md); if (has("--diff-summary")) printDiffSummary("C", prev, manifest); +if (driftWarnings.length > 0) { + for (const w of driftWarnings) console.warn(`[C] drift: ${w}`); +} console.log( - `[C] wrote docs/appendix/C.md + manifest (events=${eventInfo.events.length}, cmd_types=${cmdTypes.length})`, + `[C] wrote docs/appendix/C.md + manifest (events=${eventInfo.events.length}, cmd_types=${cmdTypes.length}, with_trigger=${manifest.counts.events_with_trigger})`, );