挂钩点能改什么,由事件合同决定
一次 turn 会经过十一个挂钩。协议认四种处理器,运行表只装命令和 MCP。超时默认放行,拆卸期丢掉 stdout。
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 挂了」来当拦截。