挂钩点能改什么,由事件合同决定
一次 turn 会经过十一个挂钩。协议认四种处理器,运行表只装命令和 MCP。超时默认放行,拆卸期丢掉 stdout。
本页解决的问题
先给结论「挂钩点能改什么,由事件合同决定」要解决的关键问题是什么?
一次 turn 会经过十一个挂钩。协议认四种处理器,运行表只装命令和 MCP。超时默认放行,拆卸期丢掉 stdout。
跟着交接处走,不要只看 Demo。 系统是否可靠,往往取决于模型、工具、状态、权限和人的交接处。把每次交接都当成可以观察、测试和恢复的地方。
为一个自动化步骤写清输入、负责人、审批和恢复动作。
一次运行成功了,却说不清发生了什么,也无法安全重放。
SessionEnd 为什么不读输出。挂钩点是生命周期上预留的插口,能改下一步的是事件合同,不是配置文件里的名字。
- 协议枚举列出十一个事件名protocol.rs L1510
- 配置层认四种 type,后两个是空结构体hook_config.rs L183
- 发现阶段把 Prompt 和 Agent 写成 not supported yetdiscovery.rs L626
- 运行表只收下 Command 和 McpToolengine/mod.rs L107
- 只有同步 hook 能施加控制效果engine/mod.rs L146
- 超时写入 error,should_block 保持 falsecommand_runner.rs L317
- 退出码 2 加 stderr 才标成 Blockedpre_tool_use.rs L261
- Allow 映射成一次性 Approvedapprovals.rs L465
- Stop 的 block 带 prompt 才在轮次层 continueturn.rs L509
- SessionEnd 退出码 0 即完成,丢掉 stdoutsession_end.rs L109
你刚从 Claude Code 把一份 hooks.json 搬过来。文件里有三条处理器:命令拦危险 shell,提示词让小模型审用户提交,agent 在 Stop 时再起一个子会话跑 linter。Claude 那边三条都能跑。
贴进 Codex,启动会话。命令那条亮了。后两条日志各写一句 not supported yet。协议枚举明明列着 Prompt 和 Agent,配置解析也认这两个 tag。装进运行表的只有命令和 MCP 工具。
内核对外有三张表,宽度不一样。
协议面列出十一个事件名,serde 走 snake_case。旁边四个处理器类型也在:Command、McpTool、Prompt、Agent。
出处:codex-rs/protocol/src/protocol.rs 第 1508 至 1531 行
配置面用 type 标签把这四个名字都接住。后两个是空结构体。解析能过,字段里没有可执行内容。
出处:codex-rs/config/src/hook_config.rs 第 183 至 187 行
发现阶段看见后两个就 continue,文案是 prompt hooks are not supported yet 和 agent hooks are not supported yet。引擎里真正可执行的种类只有命令和 MCP 工具。普通用户配置里,这两条只进 warning,会话继续。托管必选 hook 配了它们,启动会失败。
出处:codex-rs/hooks/src/engine/discovery.rs 第 626 至 645 行
JSON hook 的运行时类型名直接写成 ClaudeHooksEngine。兼容不是注释里的愿望,是类型名。stdin 喂 JSON,stdout 按 schema 解析。旁边还留着一条旧 notify 路,只在 turn 收工时 fire-and-forget 一条命令。两条路不要混。
出处:codex-rs/hooks/src/engine/mod.rs 第 107 至 119 行
配置面可以比执行器宽。先把生态里已有的四个 type 接住,未知 tag 才不会把整份 hooks.json 打爆。代价是搬家的人会按枚举名理解能力。所以发现阶段必须留下稳定文案,必选策略碰到未实现 type 必须拒绝启动。换个语言重写,最小形态仍是两张表加一个 skip。
有人写了一条 PreToolUse 脚本,超时设成 1 秒,脚本里 sleep 5 秒。他们以为 hook 崩溃等于拦截。工具照样执行。日志里这条 hook 的状态是 failed,文案带 timed out after 1s。
run_command 超时把 error 写成 hook timed out after {n}s,exit_code 是空的。解析看见 error 只标 Failed,should_block 保持默认 false。工具注册表于是继续 handle_any_tool。
出处:codex-rs/hooks/src/engine/command_runner.rs 第 317 至 326 行
会拦的路只有两条。JSON 里给出 deny 或 block。或者退出码 2 且 stderr 非空。退出码 2 却没有理由,算失败,不拦。异步 hook 即使返回 deny,也加不上控制效果。只有同步、可信、未超时的处理器能改下一步。
出处:codex-rs/hooks/src/events/pre_tool_use.rs 第 261 至 277 行
审批路径上的规则反过来。PermissionRequest 跑在 Guardian 和用户审批 UI 之前。它不改工具输入。折叠规则是:任一 deny 立刻赢,否则保留最后一次 allow。Allow 映射成一次性 Approved,不进会话缓存。下次同样的命令还要再问。
出处:codex-rs/core/src/tools/approvals.rs 第 454 至 474 行
Claude 的 PermissionRequest 输出里有 updatedInput。Codex 把这个字段标成 reserved,看见就 fail closed。从 Claude 原样搬一条带改写的审批 hook,在这里会失败,不会改写。
一条挂掉的 linter hook 不该让所有工具停摆。可用性放在拦截可靠性前面。想改流程,必须同步、必须有明确决策。想发通知,可以异步、最多 8 个并行。审批路径对歧义输出 fail closed,因为那一层不能把看不懂的字段当成允许。
PostToolUse 想拦一次危险写入,文件已经落盘。SessionEnd 想往上下文里塞收尾说明,stdout 被丢掉。wire 枚举里也没有这个事件名。两处翻车的共同点是:挂钩点已经走过它能改的那一段。
十一个挂钩点按生命周期排开。主轴是 SessionStart → UserPromptSubmit → PreToolUse → PermissionRequest → 工具 → PostToolUse → PreCompact → PostCompact → Stop → SessionEnd。子 agent 另走一条细轴,SubagentStart 和 SubagentStop 只在 ThreadSpawn 上响。
每个点的合同不一样。
PreToolUse 能拦工具、能改输入。失败默认放行。PostToolUse 只在工具成功之后跑,block 拒绝的是结果,副作用已经发生。Stop 的 block 带着 continuation prompt,才在轮次层 continue,不重发 TurnStarted。没有 prompt 的 block 被忽略。should_stop 才把控制权交回任务壳。stop 优先于 block。
出处:codex-rs/core/src/session/turn.rs 第 509 至 538 行
UserPromptSubmit 的 block 写成 should_stop,停的是当前这条用户消息,不会拿 stderr 当下一轮 prompt。matcher 在这里被忽略。SessionStart 尊重 continue: false。SubagentStart 只做上下文注入,同样的字段被丢掉。
SessionEnd 是拆卸期通知。超时默认 1 秒,上限 3 秒,给 app-server 的五秒 shutdown 留余量。退出码 0 就是完成,stdout 整段丢掉。MCP 形态直接 skip。reason 目前写死 other。
出处:codex-rs/hooks/src/events/session_end.rs 第 20 至 24 行
hook 文本进模型之前先变成 developer 角色的片段。默认预算 2500 个近似 token。超限时全文写到临时目录,模型看见头尾预览加一行路径。写盘失败就只截断。
出处:codex-rs/hooks/src/output_spill.rs 第 53 至 91 行
挂钩点的能力跟它在生命周期的位置绑定。工具还没跑,才能改输入或跳过。工具跑完,只能改模型看见的那一截。拆卸期只有几秒,读 JSON、回灌上下文、再等 MCP,都会把关机拖过上限。于是它变成纯通知。换一套运行时,该问的仍是:这个点还来不来得及改已经发生的事。
Claude Code:二十七个事件,Prompt 和 Agent 真的会跑
还原源码里 HOOK_EVENTS 有 27 项。Codex 的十一点都在,另外还有失败后事件、通知、工作树和文件变更。execPromptHook 用小模型跑一段提示词,构造 user message 时绕开 processUserInput,避免再次触发 UserPromptSubmit。execAgentHook 会起一轮完整 query。
Claude 的运行面比配置面宽。Codex 反过来,先把四个 type 接住,执行器后补。事件名高度重合,这是对齐动作。ClaudeHooksEngine 这个类型名把产品判断写进了标识符。
DSH:插件瀑布是原生接口,hook 文件是兼容桥
DSH 的工具管道在 tools/pre-execute 和 tools/post-execute 上各开一道瀑布。原生插件能做桥能做的一切。hooks-codex 只映射五点:PreToolUse、PostToolUse、SessionStart、UserPromptSubmit、Stop。没有 rewrite,没有 PermissionRequest。type: command 以外的、异步的,解析后跳过。
DSH 先有插件再补文件兼容。Codex 先有 Claude 文件合同,再让插件往同一引擎里塞声明。Grok 用十五个事件名补观察面,is_blocking() 只对 PreToolUse 返回真,没有审批前 hook。
同一条 PreToolUse,两种返回值
项目里有一条 PreToolUse 命令 hook,matcher 对着无害的 echo。先把 timeout 设成 1,脚本里 sleep 5 秒。再把脚本改成退出码 2,并向 stderr 写 blocked by test。
推演两趟结局:工具会不会执行,模型看见什么,hook 状态分别是 failed 还是 blocked。然后解释,为什么第一种不能靠「hook 挂了」来当拦截。
「先玩一遍 · 一次 turn 里钩子按什么顺序响」的能力藏在每次交接里
「你刚从 Claude Code 把一份 hooks.json 搬过来。文件里有三条处理器:命令拦危险 shell,提示词让小模型审用户提交,agent 在 Stop 时再起一个子会话跑 linter。Claude 那边三条都能跑」说明,Agent 的表现不只由模型决定。模型、上下文、工具、状态、权限和人之间的每次交接,都会改变任务能否继续以及出了问题能否恢复。
先写清状态,再增加能力
从「贴进 Codex,启动会话。命令那条亮了。后两条日志各写一句 not supported yet 。协议枚举明明列着 Prompt 和 Agent ,配置解析也认这两个 tag。装进运行表的只有命令和 MCP 工具」出发,可以把流程拆成起始状态、下一步动作、工具返回、状态更新和停止条件。这样调试时找的是第一处丢失信息或权限的位置,而不是笼统地说“模型变笨了”。
- 协议枚举列出十一个事件名 protocol.rs L1510
- 配置层认四种 type,后两个是空结构体 hook_config.rs L183
- 发现阶段把 Prompt 和 Agent 写成 not supported yet discovery.rs L626
成功路径不能代表系统可靠
用「推演两趟结局:工具会不会执行,模型看见什么,hook 状态分别是 failed 还是 blocked 。然后解释,为什么第一种不能靠「hook 挂了」来当拦截」重放一次成功和一次失败,记录每一轮真正传入的上下文、工具结果和负责人;只要第二个人能复述这条链,系统才有可维护性。
从「先玩一遍 · 一次 turn 里钩子按什么顺序响」走到「思路一 · 认得出和跑得了拆开」
「先玩一遍 · 一次 turn 里钩子按什么顺序响」先把问题落在「同一条会话时间轴,换一种返回值,看谁还能改下一步 播放 单步 重置 这次怎么回 超时 退出码 2 Prompt 类型 Stop 续跑 SessionEnd JSON 五种返回值挂在同一条轴上。超时和没实现的 type 都改不了下一步,明确拦截和带 prompt 的 block 可以。 主轴 · 一次 turn 经过的挂钩 平行细轴 · 仅 ThreadSpawn 的子 agent 这一步拿到什么、能改什么 拿到 还没…」上;到了「思路一 · 认得出和跑得了拆开」,讨论继续推进到「你刚从 Claude Code 把一份 hooks.json 搬过来。文件里有三条处理器:命令拦危险 shell,提示词让小模型审用户提交,agent 在 Stop 时再起一个子会话跑 linter。Claude 那边三条都能跑」。两段连起来,重点就不只是记住一个结论,而是看清它成立所依赖的条件。
把这条判断带到下一个场景
分析 Agent 时,沿着状态、动作、工具结果和下一步的顺序走一遍;每次交接都要能说明信息从哪里来、由谁确认、失败时停在哪里。
- 「先玩一遍 · 一次 turn 里钩子按什么顺序响」:同一条会话时间轴,换一种返回值,看谁还能改下一步 播放 单步 重置 这次怎么回 超时 退出码 2 Prompt 类型 Stop 续跑 SessionEnd JSON 五种返回值挂在同一条轴上。超时和没实现的 type 都改不了下一步,明确拦截和带 prompt 的 block 可以。 主轴 · 一次 turn 经过的挂钩 平行细轴 · 仅 ThreadSpawn 的子 agent 这一步拿到什么、能改什么 拿到 还没…
- 「思路一 · 认得出和跑得了拆开」:你刚从 Claude Code 把一份 hooks.json 搬过来。文件里有三条处理器:命令拦危险 shell,提示词让小模型审用户提交,agent 在 Stop 时再起一个子会话跑 linter。Claude 那边三条都能跑
- 「最后的要点」:只有同步 hook 能施加控制效果 engine/mod.rs L146
最后的「最后的要点」把讨论落到「只有同步 hook 能施加控制效果 engine/mod.rs L146」。回看这条线索时,最值得保留的是:当输入、规模或风险改变,哪些判断需要重新做一遍。
我把这篇文章里的一个判断改写成了今天可以验证的小实验。比记住结论更有用的是,知道下一步要观察什么。
读完以后我先回头找它成立的条件,而不是直接把方法搬进项目。这个顺序让后面的取舍清楚很多。
如果把这个判断放到真实工作里,最先需要补的约束是什么?我想知道从阅读到第一次实践之间,哪一步最值得先做。
还没有这篇文章的讨论。