Claude Code Hooks 教學:從零打造 5 個生產級 Hook
Claude Code 大約有 95% 的時候能執行正確的操作。剩下的 5% 則包括強制推送到 main、跳過格式化工具,以及提交未通過 lint 的程式碼。Hook 透過在 Claude 工作流程的 17 個生命週期節點加入確定性閘門,徹底消除這 5% 的風險。它們每次都會觸發,毫無例外,不受提示措辭或模型行為影響。
摘要: Hook 是由 Claude Code 生命週期事件觸發的 shell 命令。PreToolUse hook 可檢查並阻止操作(exit code 2 = 阻止,exit 0 = 允許)。PostToolUse hook 則在操作完成後進行驗證與格式化。在 .claude/settings.json 中以 matcher 正規表達式和巢狀 hooks 陣列進行設定。本教學將建立五個生產級 hook:自動格式化、安全閘門、測試執行器、通知提醒,以及提交前品質檢查。
重點摘要
- 個人開發者: 從自動格式化(Hook 1)和安全閘門(Hook 2)開始。這兩個 hook 能以零維護成本防止最常見的 Claude Code 錯誤。
- 團隊負責人: 將 hook 提交至您的儲存庫中的
.claude/settings.json。每位團隊成員都能自動獲得相同的安全閘門與品質檢查。 - 安全工程師: Exit code 2 會阻止操作。Exit code 1 僅記錄警告。每個 PreToolUse 安全 hook 都必須使用
exit 2,否則無法提供任何強制力。
什麼是 Hook?
Hook 是在 Claude Code 工作階段中特定生命週期事件觸發時執行的 shell 命令。它們以純腳本的形式在 LLM 之外執行,由 Claude 的操作觸發,而非由模型解讀的提示。
四大類別涵蓋最常見的使用情境(Claude Code 共支援 17 種事件類型):
- 工作階段事件:
SessionStart和Stop在工作階段開始或結束時觸發。用於初始設定、清理和通知。 - 工具事件:
PreToolUse和PostToolUse在 Claude 使用工具(寫入檔案、執行 bash 命令或搜尋程式碼)之前和之後觸發。這是最強大的 hook,因為它們能檢查並阻止特定操作。 - 通知事件:
Notification在 Claude 產生通知時觸發。適用於將警報轉發至 Slack、桌面通知或日誌系統。 - 子代理事件:
SubagentStop在子代理(透過 Agent 工具產生)完成時觸發。Hook 同樣會對子代理的操作觸發,因此您的安全閘門會遞迴套用。
Exit code 的語意至關重要。 Exit 0 代表成功(繼續執行)。Exit 2 代表阻止操作。Exit 1 代表非阻斷性的 hook 錯誤,操作仍會繼續。每個安全關鍵的 hook 都必須使用 exit 2 才能真正執行閘門功能。
Hook 設定基礎
Hook 存放在您的設定檔中:
- 專案層級: 儲存庫根目錄的
.claude/settings.json(與團隊共享) - 使用者層級:
~/.claude/settings.json(您的個人 hook,全域套用)
JSON 結構:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/path/to/your/script.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "/path/to/another-script.sh"
}
]
}
]
}
}
每個項目都有一個 matcher(正規表達式,匹配工具名稱如 Bash、Write、Edit、Read、Glob、Grep 或 Agent)和一個 hooks hook 定義陣列。每個 hook 指定 type(shell 命令用 "command")和要執行的 command。Write|Edit 這個 matcher 會同時匹配兩種工具類型。
您也可以在 Claude Code 工作階段中使用 /hooks 斜線命令互動式管理 hook。
當 hook 觸發時,Claude Code 透過環境變數(檔案操作使用 $FILE_PATH)和 stdin(包含工具名稱、參數和工作階段中繼資料的 JSON 物件)傳遞上下文。您的腳本讀取這些資訊來做出決策。
5 個實用 Hook
以下每個 hook 都解決了我使用 Claude Code 作為主要開發工具時遇到的實際問題。所有範例都使用正確的巢狀 hook 結構。
1. 檔案編輯時自動格式化
Claude 寫出的程式碼功能正確,但偶爾會違反專案的格式規則。與其要求 Claude 重新格式化,不如在每次檔案寫入後自動執行格式化工具。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash -c 'if [[ \"$FILE_PATH\" == *.py ]]; then black --quiet \"$FILE_PATH\" 2>/dev/null; elif [[ \"$FILE_PATH\" == *.js ]] || [[ \"$FILE_PATH\" == *.ts ]]; then npx prettier --write \"$FILE_PATH\" 2>/dev/null; fi'"
}
]
}
]
}
}
Claude Code 會將 $FILE_PATH 設定為被修改的檔案。Hook 檢查副檔名並執行對應的格式化工具。Python 檔案使用 black,JavaScript 和 TypeScript 檔案使用 prettier。2>/dev/null 會抑制冗餘輸出,讓您只看到實際錯誤。
對於較大的專案,建議將行內命令移至獨立腳本以提升可讀性。
2. 危險命令的安全閘門
Bash 工具的 PreToolUse hook 會檢查 Claude 即將執行的命令,如果命令匹配危險模式則予以阻止。我寫這個 hook 的起因是 Claude 在一次重構工作階段中強制推送到了 main。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash -c 'INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r \".tool_input.command\"); if echo \"$CMD\" | grep -qE \"rm\\s+-rf\\s+/|git\\s+push\\s+(-f|--force)\\s+(origin\\s+)?main|git\\s+reset\\s+--hard|DROP\\s+TABLE|:(){ :|:& };:\"; then echo \"BLOCKED: Dangerous command detected: $CMD\" >&2; exit 2; fi'"
}
]
}
]
}
}
當此 hook 以 exit code 2 退出時,Claude Code 會取消待執行的命令。錯誤訊息會同時顯示在您的終端機和 Claude 的上下文中,讓模型理解操作失敗的原因並建議更安全的替代方案。
被阻止的模式:
- rm -rf /(從根目錄遞迴刪除)
- git push --force main 和 git push -f main(強制推送到 main 分支)
- git reset --hard(銷毀未提交的工作)
- DROP TABLE(意外的資料庫刪除)
- Fork 炸彈
請根據您的環境自訂此清單。生產資料庫需要破壞性 SQL 模式的防護。基於 CLI 的部署則需要部署命令的防護。
3. 變更後自動執行測試
當 Claude 編輯 Python 檔案時,自動執行相關測試。這能立即捕捉回歸問題,而非在三個檔案編輯之後才發現。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash -c 'if [[ \"$FILE_PATH\" == *.py ]] && [[ \"$FILE_PATH\" != *test_* ]]; then TEST_FILE=\"tests/test_$(basename \"$FILE_PATH\")\"; if [[ -f \"$TEST_FILE\" ]]; then python -m pytest \"$TEST_FILE\" -x --tb=short 2>&1 | tail -20; fi; fi'"
}
]
}
]
}
}
此 hook 檢查被編輯的檔案是否為 Python 原始碼檔案(而非測試檔案本身),使用 test_ 前綴命名慣例尋找對應的測試檔案,找到後便執行。-x 旗標會在第一個失敗時停止,tail -20 保持輸出簡潔。
注意: 此 hook 假設使用扁平的 tests/ 目錄結構和 test_ 前綴命名。對於巢狀測試目錄或不同命名慣例,請調整路徑建構邏輯。
此 hook 在重構工作階段中特別有價值,因為 Claude 會觸及多個檔案。即時的回饋迴圈能防止「修好一個東西,壞掉三個」的連鎖反應——這種情況通常發生在測試只在最後才執行的時候。
4. 工作階段結束時發送通知
長時間執行的 Claude Code 工作階段可能耗時數分鐘。與其盯著終端機,不如在工作階段結束時收到通知。
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code session ended\" with title \"Claude Code\"'"
}
]
}
]
}
}
這個 macOS 範例使用 osascript 觸發原生通知。Linux 系統請將 osascript 那行替換為 notify-send "Claude Code" "Session ended"。若要發送 Slack 通知,請使用 webhook:
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-type: application/json' \
-d '{"text": "Claude Code session ended"}'
我在使用 & <task>(Claude Code 的背景模式)啟動的背景任務中使用 Slack 版本。桌面通知則用於互動式工作階段。
5. 提交前品質檢查
在 Claude 執行 git commit 之前,驗證程式碼是否通過 linting。這能捕捉到僅靠格式化無法發現的問題:未使用的匯入、未定義的變數、型別錯誤。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash -c 'INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r \".tool_input.command\"); if echo \"$CMD\" | grep -qE \"^git\\s+commit\"; then if ! LINT_OUTPUT=$(ruff check . --select E,F,W 2>&1); then echo \"LINT FAILED -- fix before committing:\" >&2; echo \"$LINT_OUTPUT\" >&2; exit 2; fi; fi'"
}
]
}
]
}
}
此 hook 僅在 Bash 命令以 git commit 開頭時觸發。它執行 ruff(快速的 Python linter)並啟用錯誤、pyflakes 和警告規則。如果存在任何問題,提交會被阻止(exit 2),Claude 會看到 lint 輸出,通常會自行修復問題並重試。
您可以疊加多個品質檢查:mypy 做型別檢查、bandit 做安全掃描,或專案自訂的驗證腳本。Bash 命令的 PreToolUse hook 為您提供了一個可程式化的閘門,在任何 shell 操作之前生效。
Hook 除錯技巧
Hook 靜默失敗的頻率比您預期的更高。以下是我用來除錯的五個技巧:
- 先獨立測試腳本。 手動將範例 JSON 管道傳入腳本:
echo '{"tool_input":{"command":"git commit -m test"}}' | bash your-hook.sh。如果在 Claude Code 外部就失敗了,在內部也一樣會失敗。 - 使用 stderr 輸出除錯資訊。 Hook 寫入 stderr 的任何內容都會出現在 Claude 的上下文中。開發時寫入
echo "DEBUG: matched $CMD" >&2,確認 hook 穩定後再移除。 - 留意 jq 失敗。 如果 JSON 路徑錯誤,
jq會靜默回傳null,導致條件判斷無法匹配。請使用實際的工具輸入測試您的jq表達式。 - 驗證 exit code。 Exit 2 阻止操作。Exit 1 僅發出警告。一個不小心使用
exit 1的 PreToolUse hook 在看似正常運作的同時,實際上完全沒有強制力。預設使用寬鬆模式(exit 0),只對特定被阻止的模式使用exit 2。 - 保持 hook 快速。 Hook 是同步執行的。一個耗時 5 秒的 hook 會為每次匹配的工具使用增加 5 秒延遲。我將所有 hook 控制在 2 秒以內,理想情況下低於 500 毫秒。
下一步
這五個 hook 涵蓋了基本面:格式化、安全、測試、通知和品質閘門。一旦您熟悉了這些模式,就可以建立用於上下文注入(在工作階段開始時新增專案特定指示)、遞迴防護(防止無限子代理迴圈)和工作流程編排(串連多步驟流程)的 hook。
如需了解 hook 架構、全部 17 個生命週期事件及進階模式,請參閱我的完整指南中的 hook 章節:Claude Code 指南:Hook 如何運作?
我也在 Claude Code Hooks:我的 95 個 Hook 各自存在的理由 中撰寫了每個 hook 背後的故事,涵蓋了促使我建立它們的各個事件。
常見問題
Hook 能阻止 Claude Code 執行命令嗎?
可以。PreToolUse hook 透過以 exit code 2 退出來阻止任何工具操作。Claude Code 會取消待執行的操作並將 hook 的 stderr 輸出顯示給模型。Exit 1 是非阻斷性的 hook 錯誤,操作仍會繼續。這個區別至關重要:每個安全 hook 都必須使用 exit 2,而非 exit 1。Claude 會看到拒絕原因並建議更安全的替代方案。
Hook 設定檔應該放在哪裡?
Hook 設定放在 .claude/settings.json 用於專案層級 hook(提交至儲存庫,與團隊共享),或放在 ~/.claude/settings.json 用於使用者層級 hook(個人設定,套用至所有專案)。當兩者同時存在時,專案層級 hook 優先。我建議使用絕對路徑指定腳本檔案,以避免工作目錄問題。
Hook 能與子代理搭配運作嗎?
可以。Hook 同樣會對子代理的操作觸發。如果 Claude 透過 Agent 工具產生子代理,您的 PreToolUse 和 PostToolUse hook 會對子代理使用的每個工具執行。若沒有這個行為,子代理就可能繞過您的安全閘門。SubagentStop 事件讓您在子代理完成任務時執行清理或驗證。