載入中
載入中

Hooks

在 CLAUDE.md 檔案中告訴 Claude 做某件事,問題在於:這只是一個請求,而不是保證。您可以寫下「編輯後務必格式化」,而 Claude 通常會聽從。通常。但在一次您沒有盯著的長時間執行中,「通常」還不夠好。Hook 可以解決這個問題。Hook 是在迴圈中固定時間點執行的確定性程式碼,因此它可以保證行為,而不是寄望於行為發生。它把一條規則從「Claude 通常會聽從」變成「Claude 無法跳過它」。

這就是整個重點。現在讓我們來看看它實際上是如何運作的。

Hook 事件

Claude Code 在一次工作階段中會觸發大約 30 個 hook 事件。您不需要全部都知道。有一小部分您會一再用到,它們恰好對應到代理迴圈中您會想要介入的那些時間點。

以下是它們在迴圈中的位置。一個工作階段開始,提示進來,工具被呼叫,最終回合結束。每一個時刻都有一個您可以掛上程式碼的 hook。

值得了解的幾個:

  • PreToolUse 在工具呼叫之前觸發。這是您的強制執行原語。它是唯一能在事情發生之前阻止它的那個。
  • PostToolUse 在工具呼叫成功之後觸發。這通常是自動格式化或自動 lint 的所在之處。
  • Stop 在 Claude 想要結束其回合時觸發。如果某個條件未滿足,您可以拒絕並說「不,你還沒完成」。還有一個對應的 SubagentStop,用於子代理完成時。
  • PreCompact 和 PostCompact 在壓縮之前和之後觸發。
  • InstructionsLoaded 在 CLAUDE.md 或規則檔案載入時觸發。對於稽核實際進入上下文的內容很方便。
  • SessionStart 在開始時觸發,並為環境做準備。如果您只想在全新啟動時觸發,請使用 startup 來源。

有一件事容易讓人搞混:要在壓縮後重新注入上下文,不要使用 PostCompact。請使用帶有 compact 比對器的 SessionStart。那才是真正能把輸出送回對話中的那個。

PreToolUse:以 JSON 回傳決策

PreToolUse 是真正強大之處,因為它可以在工具呼叫執行之前就阻止它。您回應 Claude 的方式是印出 JSON 並以零結束。關鍵欄位是 permissionDecision,它接受三個值之一:

  • allow — 讓呼叫通過
  • deny — 停止呼叫
  • ask — 交還給使用者決定

技術上還有第四個值 defer,但它只適用於非互動式的 -p 執行,在這種情況下,呼叫程序會暫停工具並在稍後恢復它。您很少會用到它。

其形狀看起來像這樣:

json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "...",
    "updatedInput": {
      "command": "..."
    }
  }
}

請注意 updatedInput。您可以改寫呼叫,而不是阻擋它。這就是您如何從 bash 指令中遮蔽一個機密資訊,同時仍然讓它執行的方法。有一個要注意的地方:updatedInput 會取代整個輸入物件,所以您必須把沒有變更的欄位也一併回傳,否則就會遺失它們。

結束代碼,適用於不回傳 JSON 的 hook

並非每個 hook 都需要使用 JSON。對於較簡單的 hook,結束代碼就能完成工作。有三個重要的數字。

  • 0 代表成功。 如果標準輸出是 JSON,Claude 會解析它。在大多數事件中,純文字會被忽略,但在 SessionStart、UserPromptSubmit 和 UserPromptExpansion 上,純文字會被加入上下文。這正是讓狀態保存 hook 運作的原因。
  • 2 代表阻擋性錯誤。 標準錯誤會被回饋給 Claude 作為上下文。這幾乎在所有地方都是阻擋性的結束代碼。
  • 其他任何值都是非阻擋性的。標準錯誤會被記錄下來,而 Claude 會繼續進行。

讓人意外的是結束代碼 1。它感覺像是一個錯誤,但它並不會阻擋。Claude 仍然會執行該指令。所以如果您的意圖是要停止某件事,請使用結束代碼 2,而不是 1。

還有幾個細節。結束代碼 2 甚至可以阻擋 Stop,這就是您告訴 Claude 它還沒完成的方式。但 PostToolUse 是在工具已經執行之後才觸發,所以在那裡阻擋已經太遲,無法阻止該次呼叫,不過它仍然可以把文字回饋給 Claude。還有少數事件完全忽略阻擋,例如 Notification 和 SessionStart。SessionStart 只會把您的標準錯誤顯示給您看,Notification 則會忽略它,而兩者都會照常繼續進行。

一個實際的防護機制:遮蔽而非阻擋

讓我們用一個實際的例子把這些串連起來。假設您想在 Bash 工具上設置一個 PreToolUse 防護機制。比對器用來挑選要監看的工具,而一個可選的 if 子句可以將範圍縮小到特定指令。

顯而易見的做法是回傳 deny 並阻止一個危險的呼叫。這樣做很好。但較少人知道、也更有趣的做法,是回傳 updatedInput 來改寫該次呼叫。這就是您如何從指令中剝除一個機密資訊,同時仍然讓它執行,而不是單純拒絕它的方法。

以下是這在實務上的樣子。Claude 被要求執行一個包含看起來像是真實機密資訊的指令。Hook 攔截了它,偵測到 sk_live_ 的模式,並在該指令真正執行之前,把它換成一個佔位符。

指令仍然執行了。工作仍然完成了。但機密資訊從未被傳遞出去。這就是阻擋與遮蔽之間的差異,也是 hook 每一次都能強制執行的那種事情。

在壓縮過程中保留狀態

還有一個值得設置的模式。當 Claude 壓縮一段長對話時,它會丟失很多細節。帶有 compact 比對器的 SessionStart hook 會在壓縮之後立即執行。讓它印出您一直在處理的檔案的簡短摘要。該摘要會回到上下文中,所以 Claude 會從它停下的地方繼續,而不是從頭開始。

總結

Hook 把一條 Claude 通常會遵循的規則,變成一條它永遠會遵循的規則。不要只停留在自動格式化:用 PreToolUse 守護工具、用 Stop 把關回合、並在壓縮過程中保留狀態。這樣的設置需要一些前期的努力,但在它第一次於您甚至沒有盯著的執行中抓到問題時,就會值回票價。