Hook 已經設定好了,指令 /hooks 也能看到它,但接下來就開始出怪事。您送出一個請求,可 requests.log 裡卻沒有出現新的一行。或者出現了兩次。有時更糟:自動動作在每次改動之後都會觸發,開始給日常工作添亂。

遇到這種情況,不必把整套設定重新搭一遍。更好的做法是把觀察到的現象變成一個可核實的小任務:什麼應該觸發這個 hook、應該留下什麼樣的痕跡,以及哪種情況它必須跳過。這樣 Claude Code 就能檢查具體檔案,提出最小的修正。

什麼時候 hook 應該觸發

這篇文章討論的是設定檔裡的命令型 hooks。這種 hook 有三個重要的部分:事件、觸發條件和處理程式。事件回答"什麼時候看",條件縮小符合的情況,處理程式執行動作。

舉例來說,請求日誌訂閱的是 UserPromptSubmit。這個事件在工作階段收到請求、模型還沒處理它之前就會觸發。大多數時候這是您的請求,但 Claude Code 自己開始的回合也會觸發它:/loop 的下一輪、背景子代理的回報。所以日誌裡多出的一行能確認的事情很少 - hook 收到了請求並記下來,僅此而已。代理有沒有把任務做對,這一行看不出來。

UserPromptSubmit 沒有 matcher。如果日誌只需要部分請求,就得由處理程式自己去篩選。工具類事件的情況不一樣。比如 matcher Edit|Write 選的是工具,檔案路徑要另外根據呼叫資料檢查。帶工具名稱的 matcher 不代表"只有 src 裡的檔案"。

核對事件和結果

事件 Stop 代表 Claude 回覆完畢。這是發通知的好時機,但不能證明程式碼正確或任務真的成功了。

修正之前,先寫下兩個簡短的情境:

  • 該發生的:送出一個測試請求,在 requests.log 裡看到一行新記錄;
  • 不該發生的:在一般編輯器裡打開檔案,不送出新的請求給 Claude Code,確認日誌沒有變化。對於記錄所有請求的日誌,第二次送出的 prompt 也應該留下記錄;它不能當作反例。

這些就是驗證的標準。不用提前猜 Claude Code 具體會怎麼回答。

記錄沒有出現

先從觀察和期望不一致的地方開始。對日誌來說,這就是測試請求之後沒有新的一行。接下來從設定往動作方向排查:

  1. 在 Claude Code 的互動式工作階段裡輸入 /hooks,檢查所需的記錄是不是已經載入。這個選單由您自己打開;把查看的結果交給代理。
  2. 核對事件。請求日誌應該對 UserPromptSubmit 起反應,而檔案改動之後的動作通常綁定在工具類事件上。
  3. 找到原始設定檔。可能是使用者、專案或本機層級的設定;不只比對名稱,也要比對完整的指令。同一個處理程式同時出現在多個設定檔裡,本身不代表會執行兩次。
  4. 檢查處理程式的指令:檔案存不存在,指令適不適合您的作業系統,帶空格的路徑有沒有出問題。
  5. 檢查結果落在哪裡。相對路徑取決於執行時的工作目錄,所以處理程式寫入的位置可能和您找的地方不一樣。

這裡的主要方法是交給代理處理。把觀察到的現象、/hooks 的檢查結果和修改的邊界一起交給它:

Hook UserPromptSubmit 在 /hooks 裡看得到,但測試請求之後
requests.log 裡沒有新的一行。

找出原始設定和處理程式。檢查事件、指令的路徑、
工作目錄,以及日誌目錄有沒有被建立。說明原因,
並直接做最小的修正。其餘的 hooks 和設定都保留。
修正之後,分別說明正例和反例兩個驗證情境。
核對修改的位置

請代理說出它要改哪個檔案、具體改哪幾行。這樣您才看得出來,它改的到底是原始設定,還是一份碰巧找到、根本不會被載入的副本。

舉個例子,代理查出來:處理程式寫入的是 logs/requests.log,但 logs/ 資料夾不存在,腳本也沒有建立它。最小的修正就是在寫入之前建立這個資料夾。修正之後,請專門檢查 logs/requests.log:一次測試請求應該剛好新增一筆記錄。這只是可能原因的一個例子,不是所有空日誌的通用診斷。

如果設定確實在檔案裡,但 /hooks 裡看不到,原因可能出在 JSON 語法、設定的作用範圍,或者 hooks 目前根本不會執行:專案資料夾沒有被標記為信任、啟用了 disableAllHooks,或者管理員只允許集中管理政策裡的 hooks。不同設定檔裡的 hooks 會合併在一起,而不是互相取代。如果記錄已經載入,卻沒有留下痕跡,重點就要轉到指令、輸入資料和輸出路徑上。

依 /hooks 選單裡的檢查結果分支的 hook 診斷流程請求之後沒有新的一行/hooks 裡看得到這筆記錄嗎?核對事件、來源和指令在設定裡找問題JSON、作用範圍、資料夾信任檢查處理程式指令、stdin、結果的路徑否是
/hooks 裡的結果決定接下來該往哪一層繼續找

這個分支的道理很簡單:Claude Code 還沒載入記錄之前,先去查腳本本身還太早。記錄已經看得到的時候,設定已經傳到應用程式了,這時檢查處理程式的執行、輸入的 JSON,以及結果該出現的位置,會更有用。

處理程式執行失敗

命令型 hook 透過標準輸入接收 JSON。裡面有事件的共通欄位,也有這個事件特有的資料。對 UserPromptSubmit 來說,請求的文字會出現在 prompt 欄位裡。這段文字必須當作資料來讀。不能把它拼接進 shell 指令然後執行:測試請求裡可能有引號、換行,以及任何一般文字。

請 Claude Code 用虛構的輸入重現處理程式的執行過程。在請求裡直接列出重要的情況:

在工作階段之外,單獨用虛構的 JSON 測試這個 hook 處理程式。
需要涵蓋的情況:一般和多行的 prompt、帶空格的專案路徑、
缺少的日誌目錄,以及格式錯誤的輸入。

不要把 prompt 的文字當成程式碼執行。顯示執行的指令、退出代碼、
stderr,以及測試檔案實際的變化。不要動到正式的日誌。

退出代碼有助於了解處理程式本身的結果,但它的意義取決於事件。exit 0 代表 hook 指令執行完畢、沒有出錯。這不能證明整個任務的結果。exit 2 在"之前"類事件上可以阻擋動作,但工具已經執行完之後,它就沒辦法撤銷結果了。所以用 exit 2 的同一套做法,不能盲目地從一個事件搬到另一個事件。

如果單獨執行成功,但在工作階段裡 hook 還是沒有動靜,請從專案資料夾用 claude --debug-file hook-debug.log 啟動 Claude Code,再重複一次測試請求。打開產生的 hook-debug.log,找出事件、符合的 hooks 和退出代碼。這是 Claude Code 自己的日誌,和您的 requests.log 是分開的。檢查完之後,結束這個診斷用的工作階段;平常工作時不要帶這個旗標啟動 Claude Code。大多數事件的一般 stdout 會進到 debug log,不一定會出現在 transcript 裡。例外情況取決於事件本身,所以任意的診斷字串寫到 stdout 裡,可能反而影響上下文,而不是變成方便的日誌。

診斷和結果分開

第一輪排查時,把診斷用的標記寫進另一個本機檔案比較安全,查完再刪掉它。官方文件裡沒有給 hooks 用的通用 dry-run 旗標;試跑模式要做成某個具體教學用處理程式的一部分。

Hook 多觸發了一次

同一個請求之後出現兩行一模一樣的記錄,可能代表重複註冊了:比如相似但不完全一樣的 hooks 分別放在使用者層級和專案層級的設定裡。如果是完全同一個指令,只是重複寫在不同檔案裡,執行起來還是只會跑一次。來自 plugin 或 skill 的副本是另外計算的,所以選單裡顯示的來源也很重要。先把 /hooks 的結果交給代理,請它列出這個事件在所有可用設定檔裡的記錄。如果設定沒問題,再檢查是不是處理程式本身在一次執行裡寫了兩次。現在就刪掉最先找到的那一行還太早。

另一個常見的原因是條件設得太寬。假設 hook 應該在 src/ 底下的檔案改動之後留下診斷標記,但實際上只要用到 Edit 或 Write 就會觸發。這裡需要兩層篩選:

  • matcher 選出 Edit 和 Write 這兩個工具;
  • 另一個條件根據工具呼叫的參數檢查路徑,把 src/ 以外的都排除掉。
PostToolUse hook 的兩層篩選PostToolUse 事件工具是 Edit 或 Write 嗎?跳過這個 hook路徑在 src/ 裡面嗎?檢查呼叫的參數不留下標記寫入標記否是否是
Matcher 選出工具,另一個檢查再縮小檔案範圍

一個檢查不能取代另一個。Matcher 先篩掉不合適的工具,接著根據參數的條件只留下 src/ 底下的路徑。只有兩個檢查都給出"是"的答案,診斷標記才會出現。

設定裡的片段可能長這樣:

"PostToolUse": [
  {
    "matcher": "Edit|Write",
    "hooks": [
      { "type": "command", "command": "python .claude/hooks/mark-source-edit.py" }
    ]
  }
]

這是既有 settings.json 裡 hooks 物件的一個元素,不是完整的 JSON 文件。路徑檢查應該放在處理程式裡;這裡的 mark-source-edit.py 腳本是假設的,文章裡沒有它的內容。這個指令假設環境裡有 python 可用,而且是從專案根目錄執行的。代理應該核對您自己設定裡的直譯器、shell 和路徑;不要把這個範例當成現成可用的設定直接照搬。

這段片段可以當成一句話來讀:

  • 檔案成功改動之後
  • 如果工具是 Edit 或 Write
  • 執行 mark-source-edit.py

這條鏈只顯示設定裡實際定義的內容。"留下標記還是跳過檔案"這個決定,是處理程式在檢查路徑之後才做的。所以有 matcher 只能確認工具選對了,還不能證明 src/ 這一層篩選是正確的。

動作預期留下的痕跡檢查的是什麼
用 Edit 改動 src/card.js新增一筆診斷標記合適的工具和合適的路徑都通過條件
用 Write 建立 src/theme.css新增一筆診斷標記另一個允許的操作也涵蓋到了
用 Edit 改動 README.md沒有新增標記src/ 以外的檔案被排除

這個表格裡的 Edit 和 Write 指的是 Claude Code 的工具。在一般編輯器裡手動改檔案不會呼叫它們,所以也驗證不了這樣的 matcher。每次動作之前先記下診斷檔案目前的結尾,之後只比對新增的記錄。

在邊界還沒驗證清楚之前,讓動作保持無害:只留下一個觸發過的標記,不做格式化、封鎖或大量修改檔案。先確認該發生的情況剛好觸發一次,不該發生的情況一次都不觸發。

多餘的執行總會增加工作量。不過,只是單純寫檔案的本機指令,本身不會去呼叫模型。prompt 或 agent 類型的 hook 不一樣,會用到模型。這裡不需要細講成本問題,只要別把這兩種處理程式混為一談就行。

怎麼關掉這個反應

如果 hook 現在就在添亂,找到它的原始記錄,只從對應事件的陣列裡刪掉這一筆。其他 hooks 都保留不動。這是關掉今後的執行:它不會撤銷已經跑過的指令,不會還原改動過的檔案,也不會刪掉已經寫進日誌的那些行。

刪除之前,如果之後還想恢復,先留一份選中記錄的副本。改完之後再打開一次 /hooks,確認這筆記錄真的沒有了。如果選單還顯示舊的設定,就結束這個工作階段,從同一個專案重新開一個新的。接著執行一個對照動作,比較改動前後的日誌。已經關掉的 hook 不應該再產生新的一行。

要暫時關掉所有 hooks,有個設定叫 "disableAllHooks": true。但這是一個總開關,不是關掉單一筆記錄的方法。這個設定本身仍然遵守設定檔的優先順序:專案檔案裡的 "disableAllHooks": false 會蓋過您使用者設定裡的 true。另外,由組織的集中管理政策設定的 hooks,一般的使用者或專案層級設定是關不掉的。

別把關掉和還原改動搞混

關掉的 hook 只是以後不會再執行。如果處理程式已經改過某個檔案,還原那個檔案是另一個步驟,而且要先看過 diff 或已儲存的版本才能做。

怎麼把修正交給 Claude Code

好的任務描述要包含觀察到的現象、預期的事件、具體的檔案,以及修改的邊界。這樣就足以讓代理去調查設定,而不用把整套自動化重寫一遍:

處理這個專案裡的 hook 問題。

觀察:一次測試請求之後,requests.log 多了兩行。
這個工作階段沒有執行 /loop,也沒有背景子代理。
期望:UserPromptSubmit 應該剛好新增一行。
我會附上 /hooks 的結果。檢查所有可用的設定來源
和處理程式的指令。

先說出原因和需要修改的檔案。然後做出最小的修正,
保留其餘的 hooks,並顯示 diff。
分別測試正例和反例。之後告訴我,
怎麼只關掉這一個反應。

最後您應該得到四個清楚的答案:

  • 哪個事件觸發這個 hook;
  • 哪個條件會跳過不需要的情況;
  • 原始設定和處理程式放在哪裡;
  • 怎麼只關掉這一個反應,不影響其他的。

還有完整的驗證流程:該發生的動作留下一個預期的痕跡,不該發生的完全不留痕跡,處理程式的錯誤能在針對性的診斷裡看到,關掉之後不再出現新的痕跡。整件事都很樸實。正因為如此,這樣的 hook 比那種只是"偶爾做點什麼"的自動化更容易維護。

要核對行為,請用官方的hooks 參考文件和實作指南:介面會隨著 Claude Code 一起變動。

行為已於 2026 年 9 月 18 日對照官方文件核實。範例展示的是檢查的流程;這裡沒有現成可用的處理程式。指令和路徑請在您自己的作業系統和 shell 裡驗證。