一次 Claude Code 會話會觸發 30 個 hook 事件。只有 3 個能回話。
Claude Code hook 事件的完整清單:每個事件何時觸發、其中哪 15 個能阻斷,以及那條悄悄吞掉大部分 hook 輸出的 stdout 規則。一份在數千次代理會話的生產環境中跑出來的實戰參考。
給 Claude Code 接 hook 的時候,有兩種故障反覆出現,而且它們看上去毫不相干。
第一種:你加了一個 hook,什麼都沒發生。沒有報錯,沒有警告,沒有日誌行。這個 hook 就是從來沒跑過。
第二種:hook 明明跑了,你能在磁碟上看到它的副作用,但它打給代理看的那條訊息始終沒送到。代理表現得就像這個 hook 什麼都沒說過。
兩者的根源是同一個。hook 系統比大多數文章覆蓋的那幾個事件更龐大、也更不統一,而決定誰能對代理說話的規則並不是你會猜到的那一套。這就是我們當初希望有人寫給我們的參考。我們在這些 hook 之上建置了 AgentsRoom,下面的每一條要麼引自官方參考文件,要麼是在生產環境裡量出來的。
是 30 個事件,不是六個
大多數指南講的是 PreToolUse、PostToolUse、UserPromptSubmit、Stop、Notification 和 SubagentStop。這六個是真實存在的,也承擔了大部分有用的工作。它們同時也只佔全部事件的五分之一。
完整清單,按各自觀察的物件分組:
| 分組 | 事件 |
|---|---|
| 會話 | SessionStart、SessionEnd、Setup |
| 提示 | UserPromptSubmit、UserPromptExpansion |
| 工具 | PreToolUse、PostToolUse、PostToolUseFailure、PostToolBatch |
| 許可權 | PermissionRequest、PermissionDenied |
| 回合 | Stop、StopFailure |
| 子代理與任務 | SubagentStart、SubagentStop、TaskCreated、TaskCompleted、TeammateIdle |
| 上下文 | PreCompact、PostCompact、InstructionsLoaded |
| 環境 | FileChanged、CwdChanged、ConfigChange |
| Worktree | WorktreeCreate、WorktreeRemove |
| 介面 | Notification、MessageDisplay |
| MCP elicitation | Elicitation、ElicitationResult |

事件在同一次會話中的觸發順序。工具塊每次工具呼叫重複一遍,整個提示塊每回合重複一遍。
其中幾個事件會改變你對這套系統的理解。PostToolUseFailure 是存在的,所以「工具到底成沒成功」這條分支本身就是一個事件,而不是你從 payload 裡推斷出來的東西。PostToolBatch 在一批並行工具呼叫結束後觸發一次,正好適合把 linter 跑一次,而不是每次編輯都跑一次。InstructionsLoaded 在 CLAUDE.md 被讀取時觸發,於是你有了一個掛載點,用來確認代理真的載入了你以為它載入了的那套規則。
吞掉大部分 hook 輸出的那條 stdout 規則
這是本頁最有用的一點。
退出碼為 0 時,Claude Code 會解析 stdout 裡的 JSON 輸出欄位。但這份 stdout 會不會被展示給代理,取決於是哪個事件,而例外只有短短一串。參考文件原文:
對大多數事件而言,stdout 會寫入除錯日誌,但不會顯示在轉錄記錄裡。例外是
UserPromptSubmit、UserPromptExpansion和SessionStart,在這三個事件上 stdout 會作為上下文加入,Claude 能看到它並據此行動。
三十個事件裡只有三個。如果你在 PostToolUse 的 hook 裡 echo "warning: this migration is destructive",指望代理讀到它,那它永遠讀不到。你的文字進了除錯日誌。
從其他任何事件把文字送到代理面前,辦法恰好只有兩個:
- 以 2 退出並寫 stderr。 退出碼為 2 時,Claude Code 會忽略 stdout 以及其中的任何 JSON,並把 stderr 作為錯誤訊息回餵給代理。
- 以 0 退出並列印一個 JSON 物件,其中攜帶
hookSpecificOutput.additionalContext。
注意第一條裡的不對稱。退出 0 意味著 stdout 有用、stderr 沒用。退出 2 意味著 stderr 有用、stdout 被整個丟棄。把這兩者搞反,就是一個 hook 看起來完全正確卻始終啞火的原因。
其他任何退出碼都是非阻斷錯誤。轉錄記錄裡會出現一條 <hook name> hook error 提示,帶上 stderr 的第一行,執行繼續進行,完整的 stderr 落進除錯日誌。

按退出碼來看,哪條通道能到達代理。虛線那條是大家以為存在、實際並不存在的路徑。
恰好一半能阻斷
十五個事件會在退出碼 2 時停下當前操作。十五個無視它,繼續往下走。
能阻斷: PreToolUse、PermissionRequest、UserPromptSubmit、UserPromptExpansion、Stop、SubagentStop、TeammateIdle、TaskCreated、TaskCompleted、ConfigChange、PostToolBatch、PreCompact、Elicitation、ElicitationResult、WorktreeCreate。
不能阻斷: PostToolUse、PostToolUseFailure、PermissionDenied、StopFailure、Notification、SubagentStart、SessionStart、Setup、SessionEnd、CwdChanged、FileChanged、PostCompact、WorktreeRemove、InstructionsLoaded、MessageDisplay。
實際後果是:護欄應該放在 PreToolUse,絕不放在 PostToolUse。PostToolUse 是在工具成功之後才觸發的。在那裡以 2 退出並不會撤銷那次寫入,只會在破壞已經落到磁碟上的時候列印一條錯誤。你要攔住 rm -rf,能下手的地方恰好只有一個。
PostToolBatch 能阻斷而 PostToolUse 不能,這一點值得多看一眼。它意味著一次批次級別的檢查仍然可以在並行編輯落地之後叫停這一回合,這是整個系統裡最接近寫後否決的東西。
matcher 是精確匹配,直到它突然不是
matcher 欄位會根據自身包含的字元切換求值策略,而且沒有任何東西告訴你它走了哪條路。
| Matcher | 求值方式 |
|---|---|
"*"、"",或不寫 | 匹配一切 |
只有字母、數字、_、-、空格、,、| | 精確字串,或按 | 或 , 拆分的精確字串列表 |
| 其他任何情況 | 不帶錨點的 JavaScript 正規表示式 |
坑就在「不帶錨點」上。參考文件明確說這個正則是用 RegExp.prototype.test 測試的,只要值裡任何位置匹配上就算成功。所以 Edit.* 會匹配 Edit,也會匹配 NotebookEdit。如果你指的是一個工具,就寫 ^Edit$。
兩個與版本相關的行為,值得在你朝錯誤的方向查錯之前先知道:
- 逗號分隔符和對空白的容忍需要 Claude Code v2.1.191 或更高版本。
- 連字元是在 v2.1.195 才加入精確匹配字符集的。在那之前,像
code-reviewer這樣的 matcher 會被當成不帶錨點的正則,因此也會為senior-code-reviewer觸發。
在 AgentsRoom 裡,我們用 Write|Edit|MultiEdit|NotebookEdit 限定自己的檔案歸屬 hook,它會走精確字串那條路,只匹配這四個工具,別的一個都不碰。我們安裝的生命週期 hook 完全不帶 matcher,因為它們總是與我們相關。
有六個地方能定義 hook,而且它們會合並
第一反應是去找一個優先順序順序。並沒有,而這恰恰是有意思的地方。
| 位置 | 作用範圍 |
|---|---|
~/.claude/settings.json | 你的所有專案,僅限本機 |
.claude/settings.json | 單個專案,可提交到版本庫 |
.claude/settings.local.json | 單個專案,被 Claude Code 加入 gitignore |
| Managed policy settings | 整個組織,由管理員控制 |
外掛的 hooks/hooks.json | 外掛啟用期間 |
| Skill 或 agent 的 frontmatter | 該元件啟用期間 |
參考文件原文:
hook 條目在各個 settings 層級之間是合併而不是相互替換的:使用者、專案和本地 settings 各自新增自己的 hook,不會移除受管的那些,而且
disableAllHooks設定無法從受管 settings 之外禁用受管 hook。
所以專案裡的 hook 從不覆蓋全域性的那個,它只是疊加在上面。六個來源,全部是疊加的。一個 PostToolUse 格式化 hook 在你的使用者 settings 裡定義過一次、在專案裡又定義了一次,那它每次編輯就跑兩遍,唯一的症狀是你覺得有點慢。

六個來源,一個合併後的集合。這裡沒有任何東西會覆蓋任何東西。
這也解釋了為什麼 .claude/settings.local.json 是一個工具往別人專案裡裝 hook 的正確位置。它的作用範圍是專案級的,Claude Code 會把它加進 gitignore,而且不需要任何命令列引數就會被載入。AgentsRoom 的條目就寫在那裡,這樣使用者提交到版本庫的 .claude/settings.json 永遠不會被動到,他的同事也不會繼承一條與某臺機器繫結的路徑。
在生產環境跑 hook 教會我們的事
AgentsRoom 會往它開啟的每一個專案裡裝 hook,用來確定性地跟蹤代理狀態,並把被修改的檔案歸屬到正確的代理身上。有些事情只有在這個規模上才顯得出來。
不認識的事件名會被悄悄忽略。 這一點文件裡沒寫,而我們依賴它。當我們往安裝程式里加一個新的生命週期事件時,CLI 版本較舊的使用者會拿到一份 settings.local.json,裡面有一個他們的二進位制檔案從沒聽說過的事件名。什麼都不會壞,什麼都不會告警,這條條目直接被跳過。正是這一點,讓安裝程式可以安全地搶在 CLI 發版之前發布。當然,這也必然意味著一個拼寫錯誤換來的是徹底的沉默,而不是一條報錯。
agent_id 是你判斷自己身處子代理之中的依據。 這個欄位只在 hook 於子代理呼叫內部觸發時才出現。它比聽上去更重要:Stop 在子代理結束自己那一回合時也會觸發,不只是主代理結束時。一條天真的「看到 Stop 就把會話標記為完成」的規則,會在任何一個子代理第一次返回時就把整個會話判定為結束。我們跳過帶 agent_id 的回合事件,正是因為這個。
不要為當前這一回合去讀 transcript_path。 參考文件警告說,轉錄記錄是非同步寫入的,可能落後於記憶體裡的對話,所以你的 hook 觸發時,最近的那幾條訊息可能還不在裡面。Stop 和 SubagentStop 會收到 last_assistant_message,就是為了讓你永遠不必去和檔案賽跑。
hook 是唯一可靠的狀態訊號。 在有 hook 之前,我們靠扒 PTY 來判斷一個代理是在思考、在等待還是已經完成。只要 CLI 改用終端的備用螢幕緩衝區渲染,這套就崩了,而 /tui fullscreen 乾的正是這件事。hook 在任何渲染方式下的觸發都完全一致。如果你要做任何從外部觀察代理的東西,這就是該建在上面的那一層,扒終端最多隻能留作兜底。
async: true 不花錢。 一條 hook 命令可以宣告 async: true,代理不會等它。我們的 hook 以 2 秒為上限向一個本地端點發 POST,然後就返回;即使接收端的應用已經關閉,代理的回合延遲也不受影響。如果你的 hook 只負責觀察、從不做決定,就把它設成非同步,別再為它付代價。
絕不要讓 hook 往終端裡寫垃圾。 我們的腳本吞掉所有異常,包括最頂層的。一個未被捕獲的 Python 呼叫棧從 hook 裡冒出來,不只是悄悄失敗,它會在使用者幹活幹到一半的終端會話裡列印出一整段棧回溯。
超時
預設值都很寬鬆,只有三個例外不是:
| hook 型別 | 預設超時 |
|---|---|
command、http、mcp_tool | 600 s |
prompt | 30 s |
agent | 60 s |
UserPromptSubmit(command、http、mcp_tool) | 30 s |
MessageDisplay(command、http、mcp_tool) | 10 s |
SessionEnd | 所有 hook 共享 1.5 s,會被抬高以匹配某個更長的單 hook timeout,最多 60 s |
SessionEnd 的預算是最讓人意外的那個。它是一份共享預算,不是每個 hook 各自的額度,所以三個清理 hook 會一起分那 1.5 秒,除非你顯式把它調高。
簡短版
- 一共有 30 個事件。出名的只有六個。
- stdout 只在
UserPromptSubmit、UserPromptExpansion和SessionStart上到達代理。其他所有地方,要麼以 2 退出配 stderr,要麼在 JSON 裡用additionalContext。 - 15 個事件在退出碼 2 時阻斷,15 個無視它。護欄放在
PreToolUse。 - matcher 都是精確字串,直到某個特殊字元把它變成不帶錨點的正則。
- 六個 settings 來源以疊加方式合併。沒有任何東西覆蓋任何東西。
- 事件名拼錯會徹底靜默地失敗。
如果比起推理,你更想直接看著這些事件觸發,那正是我們做的東西:AgentsRoom 按代理、按專案、按執行展示每一次 hook 觸發,覆蓋數十個並行代理和子代理會話。你在自己 settings 裡配置的 hook 會原樣繼續工作,因為它跑的是真正的 CLI。
繼續閱讀
如何在開發團隊中擴充套件 AI 編碼代理
一個開發者與編碼代理的故事是生產力的故事。五個開發者與二十個代理則是協調問題。當團隊擴充套件時,首先會出現什麼問題,以及保持的設定:承諾的上下文檔案、清晰的檔案所有權、按爆炸半徑進行審查,以及你可以實際看到的成本。
閱讀全文AgentsRoom 現已支援 Kimi Code
Moonshot AI 的終端編碼代理 Kimi Code 已成為 AgentsRoom 的一級提供方。把它和 Claude、Codex、Antigravity CLI、OpenCode、Aider、Grok Build、Mistral Vibe 一起執行,並在對話中途隨時切換。
閱讀全文你還應該審查你的AI代理的程式碼嗎?
你的代理寫的程式碼比你以前合併的許多拉取請求要好。那麼你還需要逐行閱讀嗎?關於雙方的誠實論點,告訴你代理出錯的10個訊號,以及每個更改實際上值得多少審查。
閱讀全文
下載 AgentsRoom
在一個視窗中執行你所有專案的 AI 代理(Claude、Codex、Antigravity CLI、OpenCode、Aider、Grok Build、Mistral Vibe、Kimi Code)。
配套應用:隨時隨地監控你的 Agent
使用 Claude、Codex、Antigravity CLI 或其他 AI 提供商。
把 Bug 和需求直接傳送到您的公開待辦清單。
AgentsRoom 實際執行一瞥。