掛鉤點能改什麼,由事件合同決定
一次 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 掛了」來當攔截。