Claude Code 掛鉤教學:從零打造 5 個正式環境掛鉤
絕大多數時候,Claude Code 都會做出正確的動作。剩下的少數邊界情況才是麻煩:強制推送到 main、漏跑格式化工具、提交無法通過 lint 的程式碼。掛鉤在 Claude 工作流程的 31 個生命週期節點(截至 2026 年 8 月)上設下確定性的關卡,藉此消除這些邊界情況。1 本教學是我探討如何打造正式環境等級代理系統的 AI 工程系列文章之一。掛鉤每次都會觸發,毫無例外,與提示詞怎麼寫、模型如何表現都無關。
TL;DR: 掛鉤是由 Claude Code 生命週期事件觸發的 shell 指令。1 PreToolUse 掛鉤負責檢查並攔截動作(結束代碼 2 = 攔截,0 = 放行)。2 PostToolUse 掛鉤則在事後進行驗證與格式化。設定寫在 .claude/settings.json 裡,包含一個 matcher(精確的工具名稱、以 | 分隔的清單,或一段正規表示式)與一個巢狀的 hooks 陣列。3 底下的教學會做出五個正式環境掛鉤:自動格式化、安全關卡、測試執行、通知提醒,以及提交前的品質檢查。
重點摘要
- 獨立開發者: 先從自動格式化(掛鉤 1)與安全關卡(掛鉤 2)著手。這兩個掛鉤能擋下 Claude Code 最常見的失誤,而且日後不必再維護。
- 技術主管: 把掛鉤提交到儲存庫的
.claude/settings.json。團隊每位成員都會自動擁有相同的安全關卡與品質檢查。 - 資安工程師: 真正攔下動作的是結束代碼 2。2 結束代碼 1 只會記下一則警告。每個 PreToolUse 資安掛鉤都必須使用
exit 2,否則形同虛設。
什麼是掛鉤
掛鉤是在 Claude Code 工作階段期間、於特定生命週期事件執行的 shell 指令。它們跑在 LLM 之外,是由 Claude 的動作所觸發的一般指令碼,而不是交給模型解讀的提示詞。
四大類事件涵蓋了最常見的使用情境(截至 2026 年 8 月,Claude Code 已記載 31 種事件類型)。1
- 工作階段事件:
SessionStart在工作階段開始時觸發,SessionEnd在關閉時觸發,而Stop則在 Claude 每次回覆結束時觸發(不只是工作階段結束時)。適合用來做前置準備、收尾與通知。 - 工具事件:
PreToolUse與PostToolUse分別在 Claude 使用工具前後觸發(寫入檔案、執行 bash 指令、搜尋程式碼等)。它們能檢查並攔截特定動作,因此是最強大的掛鉤。 - 通知事件:
Notification在 Claude 產生通知時觸發。適合把警示轉送到 Slack、桌面通知或記錄系統。 - 子代理事件:
SubagentStop在透過 Agent 工具產生的子代理完成任務時觸發。4 掛鉤對子代理的動作同樣有效,因此安全關卡會遞迴套用。
結束代碼的語意至關重要。2 0 代表成功(繼續進行),2 代表攔截該動作,1 代表掛鉤發生錯誤但不攔截,動作照樣進行。凡是攸關資安的掛鉤,都必須使用 exit 2,關卡才真的擋得住。
心智模型:三種保證
動手寫掛鉤之前,先問自己:我需要的是哪一種保證?
格式保證在事後維持一致性。掛在 Write/Edit 上的 PostToolUse 掛鉤,會在每次檔案變動後執行格式化工具。模型輸出成什麼樣並不重要,因為格式化工具會把一切正規化。這類掛鉤具冪等性,每次編輯都跑也很安全。
安全保證在危險動作執行前將其攔下。掛在 Bash 上的 PreToolUse 掛鉤會檢查指令,並以結束代碼 2 攔截具破壞性的樣式。這類掛鉤必須夠快(500 毫秒以內),因為每一次符合條件的工具呼叫都得經過它;而且必須用 exit 2 而非 exit 1,因為 exit 1 只警告、不攔截。
品質保證在決策點驗證狀態。掛在 git commit 指令上的 PreToolUse 掛鉤會執行 linter 或測試套件,只要品質檢查沒過就攔下這次提交。與每次編輯都觸發的格式類掛鉤不同,品質類掛鉤只在特定時刻觸發,開銷因此很低。
概念上的祖先是 Git 掛鉤8: pre-commit、pre-push 與 post-commit 擔任的正是同樣的三種角色。Claude Code 的掛鉤把這個樣式從 Git 操作延伸到代理所執行的每一個工具動作。我在每個掛鉤都是一道疤裡剖析了這段演變:每個掛鉤之所以存在,都是因為少了它的時候出過事。
掛鉤設定基礎
掛鉤放在設定檔裡。
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 陣列。依照參考文件,matcher 的語意如下:"*"、"" 或省略 matcher 表示全部符合;由字母、數字、_、-、|、逗號與空格構成的值,會被視為精確的名稱或名稱清單,因此 Write|Edit 會同時符合兩個工具(對 mcp__github__search_code 這類 MCP 工具名稱而言,底線相當重要);其餘的值一律當成未錨定的正規表示式處理。比對會區分大小寫,bash 永遠不會符合 Bash。每個掛鉤要指定 type(shell 指令填 "command")以及要執行的 command。
在工作階段中,您可以用唯讀的 /hooks 瀏覽介面查看已註冊的掛鉤;若要新增、修改或移除掛鉤,請直接編輯設定 JSON。5
掛鉤觸發時,Claude Code 會以 JSON 物件的形式透過 stdin 傳入脈絡:工具名稱、工具輸入(檔案操作會包含 file_path),以及工作階段的中繼資料。6 您的指令碼從 stdin 讀取這些內容來做判斷,通常會搭配 jq。此外還會設定幾個提供脈絡的環境變數,例如用於解析路徑的 $CLAUDE_PROJECT_DIR、代表目前 effort 等級的 $CLAUDE_EFFORT;但像檔案路徑這種跟個別工具有關的欄位,只會出現在 stdin 中,並不存在按工具設定的 $FILE_PATH 變數。
5 個實用掛鉤
底下每個掛鉤,都解決了我把 Claude Code 當成主力開發工具時真正遇到的問題。所有範例都採用掛鉤參考文件中正確的巢狀結構7。
1. 編輯檔案後自動格式化
Claude 寫出的程式碼在功能上沒問題,卻偶爾會破壞專案的格式規範。我一開始試著在 CLAUDE.md 裡加上「編輯 Python 檔案後一律執行 black」,但這條指示大約只有八成的時候有效。當模型專注於跨多個檔案的複雜變更時,格式化這一步有時就被略過了。PostToolUse 掛鉤徹底消除了這種不一致:不論模型選擇怎麼做,每次寫入檔案後格式化工具都會執行。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash -c 'FILE=$(jq -r \".tool_input.file_path // empty\"); if [[ \"$FILE\" == *.py ]]; then black --quiet \"$FILE\" 2>/dev/null; elif [[ \"$FILE\" == *.js || \"$FILE\" == *.ts ]]; then npx prettier --write \"$FILE\" 2>/dev/null; fi'"
}
]
}
]
}
}
這個掛鉤從 stdin 讀取工具的 JSON 輸入,並用 jq 取出 .tool_input.file_path——由於 Claude Code 不會設定按工具區分的環境變數,那個 stdin 物件是檔案路徑唯一存在的地方。接著它檢查副檔名,執行對應的格式化工具:Python 檔案交給 black,JavaScript 與 TypeScript 檔案交給 prettier。2>/dev/null 會壓下吵雜的輸出,讓您只看到真正的錯誤。
在較大的專案裡,把這段內嵌指令搬到獨立的指令碼中會更好讀。
2. 攔截危險指令的安全關卡
掛在 Bash 工具上的 PreToolUse 掛鉤,會檢查 Claude 即將執行的指令,一旦命中危險樣式就攔下來。這個掛鉤的第一個版本,是在某次重構的工作階段中 Claude 強制推送到 main 之後寫的。(關於代理自主性更廣泛的意涵,我在利爪解剖與作為基礎設施的 Claude Code中討論過。)當時模型收到的要求是「把改動推上去」,而因為分支已經分岔,它把這句話解讀成了 git push --force origin 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|:\\(\\)\\s*\\{\\s*:\"; then echo \"BLOCKED: Dangerous command detected: $CMD\" >&2; exit 2; fi'"
}
]
}
]
}
}
當這個掛鉤以結束代碼 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 'FILE=$(jq -r \".tool_input.file_path // empty\"); if [[ \"$FILE\" == *.py && \"$FILE\" != *test_* ]]; then TEST_FILE=\"tests/test_$(basename \"$FILE\")\"; if [[ -f \"$TEST_FILE\" ]]; then if ! OUT=$(python -m pytest \"$TEST_FILE\" -x --tb=short 2>&1); then echo \"TESTS FAILED after editing $FILE:\" >&2; echo \"$OUT\" | tail -20 >&2; exit 2; fi; fi; fi'"
}
]
}
]
}
}
這個掛鉤會從 stdin 的 JSON 取出被編輯檔案的路徑,判斷它是不是 Python 原始碼檔案(而非測試檔案本身),再依 test_ 前綴的命名慣例尋找對應的測試檔案,找到就執行。-x 旗標會在第一次失敗時停止,tail -20 則讓輸出保持精簡。真正讓這個掛鉤有用的,是失敗時 exit 2 的那一段:PostToolUse 掛鉤無法還原已經發生的編輯,但 exit 2 會把失敗的測試輸出透過 stderr 交給 Claude,Claude 便會在繼續下一步之前修好這處毀損。若只是在 exit 0 的情況下印出失敗訊息,那些內容就進了除錯記錄——沒人會去看。
注意: 上面的掛鉤假設測試放在扁平的 tests/ 目錄,並採用 test_ 前綴命名。若專案的測試結構與原始碼樹一一對應(例如 tests/api/test_users.py 對應 src/api/users.py),請把 TEST_FILE 那一行換成:
TEST_FILE="tests/$(echo "$FILE" | sed 's|.*/src/||; s|\([^/]*\)\.py$|test_\1.py|')"
在 Claude 需要動到多個檔案的重構工作階段中,測試執行掛鉤格外有價值。少了即時回饋,錯誤就會層層堆疊:Claude 編輯檔案 A,弄壞了檔案 B 的測試,接著又依據 B 的損壞狀態去編輯檔案 C。等您發現失敗時,要修的已經不是一個檔案,而是三個。每次編輯後都跑測試,第一處毀損當場就能被抓到。
4. Claude 回覆完成時發出通知
Claude Code 的一輪對話可能耗上好幾分鐘。與其盯著終端機等,不如讓它在回覆結束時通知您。(Stop 會在每次回覆結束時觸發;若想要的是工作階段真正關閉時執行的掛鉤,請改註冊 SessionEnd。)
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude finished responding\" with title \"Claude Code\"'"
}
]
}
]
}
}
上面的 macOS 版本以 osascript 觸發原生通知。在 Linux 上,把 osascript 那一行換成 notify-send "Claude Code" "Finished responding" 即可。若要發到 Slack,可以改用 webhook:
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-type: application/json' \
-d '{"text": "Claude Code finished responding"}'
以 & <task>(Claude Code 的背景模式)發動的背景任務,我用 Slack 版本;互動式的工作階段則交給桌面通知。
5. 提交前的品質檢查
在 Claude 執行 git commit 之前,先驗證程式碼能否通過 lint。提交前的 lint 關卡能抓到光靠格式化找不出的問題:未使用的 import、未定義的變數、型別錯誤。
{
"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'"
}
]
}
]
}
}
只有當 Bash 指令以 git commit 開頭時,這道品質關卡才會啟動。它會以 error、pyflakes 與 warning 規則執行 ruff(一款快速的 Python linter)。只要存在問題,掛鉤就攔下這次提交(exit 2),而 Claude 會看到 lint 的輸出,通常便會據此修正問題並重試。
您還可以疊上多層品質檢查:用 mypy 做型別檢查、用 bandit 做資安掃描,或是跑專案自訂的驗證指令碼。掛在 Bash 指令上的 PreToolUse 掛鉤,讓您在任何 shell 動作之前都有一道可程式化的關卡。
.claude/settings.json 中的 PreToolUse 與 PostToolUse:參考
如果您是搜尋 PreToolUse/PostToolUse 的設定寫法而來到這裡,這一節就是精簡版。兩個事件都巢狀放在 .claude/settings.json(專案)或 ~/.claude/settings.json(使用者)的 hooks 鍵底下;各作用範圍會合併生效,完全相同的處理常式則會去除重複。把兩個事件寫在同一個區塊裡:3
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": ".claude/hooks/guard.sh" }]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": ".claude/hooks/format.sh" }]
}
]
}
}
三句話講清這份約定:兩個事件都透過 stdin 傳入工具 JSON(Bash 取 .tool_input.command,Write/Edit 取 .tool_input.file_path——沒有按工具設定的環境變數)。6 PreToolUse 在工具呼叫之前觸發,可以用 exit 2 把它攔下。PostToolUse 在工具成功之後觸發——它無法還原該動作,但 exit 2 會把它的 stderr 回傳給 Claude,Claude 便會修正掛鉤指出的問題。2
想看這兩個事件的完整文件——JSON 輸出欄位、permissionDecision、updatedInput、逾時設定——官方參考在 code.claude.com/docs/en/hooks;我的 Claude Code 指南中的掛鉤章節涵蓋同樣的範圍,並附上實地驗證過的做法。
猜錯事件名稱了?對照表在這裡
大家常搜尋的掛鉤事件名稱,與 Claude Code 實際觸發的事件對照如下:1
| 如果您猜的是…… | 真正的事件 |
|---|---|
onStart / onSessionStart |
SessionStart |
onFinish / onEnd / onStop |
Stop(Claude 每次回覆結束時觸發)或 SessionEnd(工作階段關閉時) |
onToolUse / beforeToolUse |
PreToolUse |
afterToolUse |
PostToolUse |
onPrompt / onUserMessage |
UserPromptSubmit |
onError |
PostToolUseFailure(工具錯誤)或 StopFailure(API 錯誤) |
事件總共有 31 個,指南中的事件對照表把它們逐一列了出來。
把 PreToolUse、PostToolUse 與 Stop 掛鉤寫進同一份設定
把最常被搜尋的三個事件接在一起——一個指令防護、一個格式化工具,以及一則完成通知:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": ".claude/hooks/guard-bash.sh" }]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "bash -c 'FILE=$(jq -r \".tool_input.file_path // empty\"); [[ \"$FILE\" == *.py ]] && black --quiet \"$FILE\" || true'" }]
}
],
"Stop": [
{
"hooks": [{ "type": "command", "command": "osascript -e 'display notification \"Claude finished responding\" with title \"Claude Code\"'" }]
}
]
}
}
guard-bash.sh 就是把掛鉤 2 的安全關卡搬進獨立指令碼的成果:把掛鉤 2 那條 bash -c 單行指令的主體(外層單引號之間的全部內容)存成 .claude/hooks/guard-bash.sh,加上 #!/bin/bash 這行 shebang,再 chmod +x;或者直接從Claude Code 掛鉤詳解取用同名的現成指令碼。每個事件都保有各自的語意:PreToolUse 防護可以否決指令(exit 2),PostToolUse 格式化工具在每次符合條件的編輯之後執行,而 Stop 掛鉤則在每次回覆結束時觸發——它不需要 matcher,因為 Stop 不是工具事件,沒有東西好篩選。1
掛鉤除錯技巧
掛鉤悄無聲息地失敗的頻率,比您以為的高。我用來除錯的五個做法:
- 先單獨測試指令碼。 手動把範例 JSON 灌進指令碼:
echo '{"tool_input":{"command":"git commit -m test"}}' | bash your-hook.sh。在 Claude Code 之外跑不通的,在裡面同樣跑不通。 - 搞清楚 stderr 究竟去了哪裡。 只有掛鉤以 exit 2 結束時,stderr 才會進入 Claude 的脈絡;exit 0 時它會落在除錯記錄裡,而其他非零的結束代碼只會在對話記錄中留下一則掛鉤錯誤的提示。開發期間請執行
claude --debug(工作階段進行中則用/debug),並盯著除錯記錄,exit 0 的掛鉤輸出就落在那裡。 - 當心 jq 出錯。 如果 JSON 路徑寫錯了,
jq9 會安靜地回傳null,您的條件判斷就一個也對不上。請用真實的工具輸入來測試jq運算式。 - 確認結束代碼。 exit 2 會攔截動作,exit 1 只是警告。一個不小心寫成
exit 1的 PreToolUse 掛鉤,看起來像在運作,實則毫無約束力。預設請放行(預設 exit 0),只對特定要攔截的樣式使用exit 2。 - 讓掛鉤保持快速。 掛鉤是同步執行的。一個要跑 5 秒的掛鉤,會為每一次符合條件的工具使用都加上 5 秒。我把所有掛鉤都控制在 2 秒以內,最好是 500 毫秒以內。
最常見的掛鉤錯誤: 把安全關卡寫成 exit 1 而不是 exit 2。測試時它看起來有效,因為警告訊息會印到終端機。但 exit 1 是不會攔截的警告,危險指令照樣執行。這個錯誤我在三個不同團隊的掛鉤設定裡都見過,而他們都以為自己已經擋下了強制推送。每個資安掛鉤都要靠實際觸發被攔截的樣式來測試,確認動作真的被阻止,而不只是收到一則警告。
下一步
這五個掛鉤涵蓋了最基本的部分:格式化、資安、測試、通知與品質關卡。等您熟悉了這些做法,就能進一步打造脈絡注入(在工作階段開始時補上專案專屬的說明)、遞迴防護(避免子代理無限迴圈)與流程編排(把多步驟的程序串起來)之類的掛鉤。
關於掛鉤架構、完整的 31 個事件生命週期以及進階做法,請參閱我那份完整Claude Code 指南中的掛鉤章節,或Claude Code 掛鉤詳解裡逐一事件的說明。
我也在Claude Code 掛鉤:我的 95 個掛鉤各自為何存在中寫下了這 95 個正式環境掛鉤背後的來龍去脈,逐一交代促成它們的那些事故。
參考資料
常見問題
掛鉤能阻止 Claude Code 執行某條指令嗎?
可以。PreToolUse 掛鉤只要以結束代碼 2 收場,就能攔下任何工具動作。Claude Code 會取消待執行的動作,並把掛鉤的 stderr 輸出秀給模型看。exit 1 屬於不攔截的掛鉤錯誤,動作仍會繼續。結束代碼的這個區別至關重要:每個資安掛鉤都必須用 exit 2,而不是 exit 1。2 Claude 看到被拒絕的理由後,會提出更安全的替代做法。
掛鉤設定檔要放在哪裡?
專案層級的掛鉤設定放在 .claude/settings.json(提交到儲存庫,與團隊共用),使用者層級的則放在 ~/.claude/settings.json(個人使用,套用到每個專案)。兩者同時存在時,掛鉤是合併而非覆蓋:來自各個作用範圍且符合條件的掛鉤都會執行,完全相同的處理常式會去除重複。建議指令碼檔案採用絕對路徑,以免受工作目錄影響。
掛鉤對子代理有效嗎?
有效。掛鉤對子代理的動作同樣會觸發。4 如果 Claude 透過 Agent 工具產生了子代理,那麼該子代理使用的每一個工具,都會執行您的 PreToolUse 與 PostToolUse 掛鉤。少了遞迴層級的強制執行,子代理就可能繞過您的安全關卡。而 SubagentStop 事件讓您能在子代理完成任務時執行清理或驗證。4
掛鉤多少個算太多?
真正的限制是效能,不是數量。每個掛鉤都同步執行,因此掛鉤的總執行時間會疊加到每一次符合條件的工具呼叫上。我在使用者層級與專案層級的設定中總共跑著 95 個掛鉤,卻感覺不到延遲,因為每個掛鉤都在 200 毫秒內完成。我盯著的門檻是:若某個 PostToolUse 掛鉤為每次檔案編輯多加了超過 500 毫秒,工作階段就會顯得遲鈍。部署之前,請用 time 為掛鉤做一次效能量測。十個快掛鉤,勝過兩個慢掛鉤。
-
Anthropic,”Hooks reference — Hook events”。code.claude.com/docs/en/hooks#hook-events ↩↩↩↩↩
-
Anthropic,”Hooks reference — Exit code output”。code.claude.com/docs/en/hooks#exit-code-output ↩↩↩↩↩
-
Anthropic,”Hooks reference — Configuration”。code.claude.com/docs/en/hooks#configuration ↩↩↩↩
-
Anthropic,”Hooks reference — Hook events”(SubagentStart/SubagentStop)。code.claude.com/docs/en/hooks#hook-events ↩↩↩
-
Anthropic,”Hooks reference — Configuration”(
/hooks選單)。code.claude.com/docs/en/hooks#configuration ↩ -
Anthropic,”Hooks reference — Hook input and output”。code.claude.com/docs/en/hooks#hook-input-and-output ↩↩
-
Anthropic,”Hooks reference — Configuration”(巢狀掛鉤結構)。code.claude.com/docs/en/hooks#configuration ↩
-
Git 官方文件,”Customizing Git: Git Hooks”。git-scm.com/book/en/v2/Customizing-Git-Git-Hooks ↩
-
jq 手冊,”Command-line JSON processor”。jqlang.github.io/jq/manual ↩