🎯 重點摘要: Hooks 是你在 Claude Code 生命週期各個時點註冊的自動執行指令——它們由系統強制執行,不靠 AI 「記得」去做:
① 確定性保證:「每次改完檔案都跑 prettier」這種規則,寫進 prompt AI 可能忘,寫成 hook 永遠不會漏
② 32 種事件:從 Session 開始/結束、使用者送 prompt、每個工具呼叫前後到 compact 前後,全程都有掛載點
③ 能擋能放:PreToolUse 可以在危險操作執行前攔下來(exit 2),Stop 可以不讓 Claude 收工
④ 雙向溝通:stdin 收 JSON 細節(哪個檔案、什麼指令),stdout 回 JSON 控制流程(放行、阻擋、注入上下文)
⑤ 安全是雙刃刃:hooks 以你的完整權限執行,別人 repo 裡的設定也能在你claude -p時直接跑——用之前先看清楚
Hooks 是什麼?為什麼需要它?
使用 Claude Code 一陣子後,你一定遇過這幾件事:「希望它每次改完 TypeScript 都自動跑 prettier」、「絕對不准碰 .env」、「它做完事能不能叫我一聲」。這些需求的共通點是:你想要「保證」,而不是「請求」。
Prompt 是軟性的——LLM 判斷後可能做、可能忘。Hooks 是硬性的——它們是你定義的 shell 指令(現在還支援 HTTP endpoint 和 LLM prompt),綁定在 Claude Code 的生命週期事件上,該觸發就一定觸發。一句社群流傳的話講得最好:
💡 Hooks 是由 harness 執行的確定性指令,不是請 AI「記得格式化」——AI 會忘,hook 不會。
官方文件對 hooks 的定位也一路升級:早期只是「shell commands」,現在是「shell 命令、HTTP endpoint 或 LLM prompt」,而且終端機、IDE 外掛、Desktop App、網頁版全部共用同一套事件系統。
快速上手:五分鐘的第一個 Hook
最經典的入門範例:Claude 每次編輯或新增檔案後,自動跑 prettier 格式化。在專案根目錄建立 .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write",
"timeout": 30
}
]
}
]
}
}
存檔後 Claude Code 的 file watcher 會自動抓到新設定(不用重啟)。接下來每當 Claude 用 Edit 或 Write 工具改完檔案,這段指令就會收到一份 JSON(從 stdin 進來)、抽出檔案路徑、交給 prettier 處理。Edit|Write 就是 matcher——只對這兩個工具生效。
設定檔位置與優先順序
Hooks 可以放在好幾層設定裡,各司其職:
| 位置 | 範圍 | 適合放什麼 |
|---|---|---|
~/.claude/settings.json | 所有專案 | 個人偏好(通知、格式化習慣) |
.claude/settings.json | 單一專案,可 commit | 團隊共享規範(lint、測試、保護清單) |
.claude/settings.local.json | 單一專案,個人 | 不想 commit 的實驗性 hooks |
| 企業 managed policy | 全組織 | 資安政策(macOS 在 /Library/Application Support/ClaudeCode/managed-settings.json) |
Plugin 的 hooks/hooks.json | plugin 啟用期間 | 隨 plugin 散布的功能 |
| Skill / Subagent frontmatter | 被叫起後的 session | 該技能專用的輔助邏輯 |
兩個關鍵行為:hooks 是跨層合併不是覆蓋——專案加上去的 hook 不會移掉 user 層的;而相同 handler 出現在不同 settings 檔只會跑一次,避免重複執行。
設定結構:三層巢狀
完整的 JSON 結構是三層:hooks → 事件陣列裡的 matcher 群組 → handler 陣列:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
"timeout": 10,
"statusMessage": "檢查 rm 指令安全性…"
}
]
}
]
}
}
Handler 的常用欄位
| 欄位 | 說明 |
|---|---|
| type | "command"(shell 指令)、"http"、"prompt"(用 LLM 判斷)、"agent" 等,最常用的是 command |
| command | 要執行的指令;配 args 使用時走 exec form 直接 spawn、不經 shell 解析 |
| matcher | 工具名過濾器,只對工具類事件有效;"Edit|Write"、"Bash"、省略代表全部 |
| if | permission rule 語法的第二層過濾,例如 "Bash(git *)"、"Edit(*.ts)" |
| timeout | 秒數。command/http 型預設 600 秒;prompt 型 30 秒;agent 型 60 秒 |
| async | true 時背景執行不阻塞主流程,適合跑測試這類慢活 |
| statusMessage | 執行中顯示在 spinner 的文字 |
⚠️ matcher 的坑:含特殊字元(如.、*)的 matcher 會被當成未錨定的 JavaScript regex——Edit.*會同時命中Edit和NotebookEdit!要精確匹配整串請寫^Edit$。另外 MCP 工具要匹配整個 server 得寫mcp__memory__.*,只寫mcp__memory反而什麼都不會中。
核心事件詳解
Hook 事件目前多達 32 種(完整清單見下一節表格),日常真正高頻使用的是下面這十個:
SessionStart
Session 開始或恢復時觸發(matcher 可選 startup/resume/clear/compact/fork)。最實用的招數:直接 echo 環境資訊,純文字 stdout 會變成 Claude 看得到的 context。官方食譜就用它在 compact 後重新提醒「這專案用 Bun 不用 npm」。
UserPromptSubmit
你送出 prompt、Claude 開始處理前觸發。可以注入上下文、做審計記錄,也可以 block(連 prompt 一起撤銷)。注意此事件的預設 timeout 只有 30 秒。
PreToolUse —— 最重要的攔截點
工具參數已產生、但還沒執行的瞬間。這是實現「不准碰某些檔案」「不准跑某類指令」政策的地方,可以用 exit 2 或 JSON 決策把動作擋下來,並讓 Claude 看到 reason 自己修正。
PermissionRequest
Claude Code 正要跳出權限詢問對話框時觸發(比通知早約 6 秒)。可以程式化自動批准特定提示——例如「ExitPlanMode 的確認永遠放行」。官方特別警告:matcher 千萬別留空或寫 .*,否則等於把所有權限提示全部自動通過。
PostToolUse
工具成功完成後觸發,附帶 tool_response 和執行時間。自動格式化、lint、觸發重建都在這裡。它擋不了已完成的動作,但 exit 2 的 stderr 會餵給 Claude 看——「格式化失敗了,語法有錯,去修」就是這樣做的。
Notification
Claude Code 發通知時觸發(權限等待、閒置待輸入、需要授權等類型)。拿來做桌面通知或手機推播最直覺。
Stop / SubagentStop
主 agent(或 subagent)回應完畢時觸發。exit 2 可以不讓它停——「測試沒過就繼續修」的自動迴圈就是這個做的。腳本裡記得檢查 stop_hook_active 欄位避免無限循環;連續擋 8 次後系統會強制結束 turn。
PreCompact
context 壓縮執行前觸發。可以在壓縮前把重要決策存檔,或乾脆擋下 compaction。
📌 冷知識:用@引用的檔案不經過 Read 工具呼叫,所以 PreToolUse 攔不到;直接打/skillname也不會經過 Skill 工具——這條路徑要用UserPromptExpansion事件才攔得到。
全部事件一覽表
| 事件 | 觸發時機 | 可阻擋 |
|---|---|---|
| SessionStart | session 開始/恢復(startup/resume/clear/compact/fork) | 否,可注入 context |
| Setup | --init-only 等一次性準備流程 | 否 |
| InstructionsLoaded | CLAUDE.md 或 rules 檔載入時 | 否,純觀測 |
| UserPromptSubmit | prompt 送出、處理前 | ✓ |
| UserPromptExpansion | /指令 展開成 prompt 時 | ✓ |
| PreToolUse | 工具執行前 | ✓ 核心攔截點 |
| PermissionRequest | 權限對話框將彈出前 | 僅 JSON decision |
| PermissionDenied | auto mode 分類器拒絕工具時 | 僅可要求 retry |
| PostToolUse | 工具成功完成後 | 否,stderr 可回饋 |
| PostToolUseFailure | 工具執行後失敗 | 否 |
| PostToolBatch | 一批平行工具全部結束時 | ✓ 可停住迴圈 |
| Notification | 發送通知時 | 否 |
| MessageDisplay | 助理訊息串流顯示時(只改畫面不改原文) | 否 |
| SubagentStart / SubagentStop | subagent 生出/完成時 | Stop 可 ✓ |
| TaskCreated / TaskCompleted | 任務建立/標記完成時 | ✓ |
| Stop / StopFailure | turn 正常結束/API 錯誤結束 | Stop 可 ✓ |
| TeammateIdle | agent team 隊友即將閒置 | ✓ |
| ConfigChange | 設定檔 session 中變更時 | ✓(policy 除外) |
| CwdChanged / DirectoryAdded | 工作目錄變更/新增目錄後 | 否 |
| FileChanged | 被 watch 的檔案磁碟變動(誰改的都算) | 否 |
| WorktreeCreate / WorktreeRemove | worktree 建立/移除(可取代預設 git 行為) | 非零即失敗 |
| PreCompact / PostCompact | context 壓縮前/後 | PreCompact 可 ✓ |
| Elicitation / ElicitationResult | MCP 請求使用者輸入/回答返回前 | ✓ 可代答或攔截 |
| SessionEnd | session 結束(clear/resume/logout/other) | 否 |
Hook 的輸入:stdin JSON
每個 hook 觸發時都會從 stdin 收到一份 JSON。所有事件都有共同欄位:
| 欄位 | 說明 |
|---|---|
| session_id | 目前的 session ID |
| transcript_path | 對話 transcript 的路徑(異步寫入可能落後,Stop 類事件建議改用 last_assistant_message) |
| cwd | 當下工作目錄 |
| permission_mode | default/plan/acceptEdits/auto/bypassPermissions 等 |
| hook_event_name | 事件名稱 |
工具類事件再加 tool_name、tool_input、tool_use_id。以 PreToolUse 觸發 Bash 為例:
{
"session_id": "abc123",
"transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
"cwd": "/home/user/my-project",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test",
"description": "Run test suite",
"timeout": 120000,
"run_in_background": false
},
"tool_use_id": "toolu_01ABC123"
}
⚠️ 跨平台陷阱:Write/Edit/Read 的file_path一律是絕對路徑,Windows 上用反斜線。腳本要比對副檔名前先正規化:FILE_PATH="${FILE_PATH//\\//}"
控制流程:退出碼與 JSON 輸出
退出碼語意(背起來)
| 退出碼 | 意義 |
|---|---|
| exit 0 | 成功。stdout 若是 JSON 會被解析為進階控制;UserPromptSubmit、SessionStart 等事件的純文字 stdout 會直接變成 Claude 的 context |
| exit 2 | 阻斷錯誤。擋下該事件允許阻擋的動作,stderr(或 JSON 的 reason)回饋給 Claude 讓它修正 |
| exit 其他 | 非阻塞錯誤。顯示 hook error 但動作照常進行 |
🚨 最重要的警告:exit 1 不會擋任何事。很多教學沒講清楚——想當政策閘門卻寫了 exit 1,等於大門敞開。腳本路徑打錯(exit 127)也一樣默默失效,部署政策型 hook 後務必實測一次「真的會擋」。
JSON 進階控制
exit 0 時 stdout 輸出 JSON 物件可以做細緻控制(解析條件:去掉前置空白後第一個字元必須是 {):通用欄位有 continue(false 直接停止整個處理)、stopReason(給使用者看的停止原因)、systemMessage(給使用者的警告)。決策類最常用的兩種:
# PreToolUse:四態決策 allow / deny / ask / defer
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": ".env 是機密檔案,禁止修改"
}
}
# UserPromptSubmit / Stop 等:頂層 decision
{
"decision": "block",
"reason": "測試還沒全過,繼續修"
}
其他好用欄位:additionalContext 注入上下文(會包成 system reminder);updatedInput 整包替換工具參數;PostToolUse 的 updatedToolOutput 甚至能改寫工具回傳結果。舊式頂層 decision: "approve"/"block" 在 PreToolUse 已棄用,新腳本請用 hookSpecificOutput.permissionDecision。
實戰食譜六道
① 保護敏感檔案(PreToolUse)
#!/bin/bash
# .claude/hooks/protect-files.sh
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path')
# Windows 反斜線正規化
FILE="${FILE//\\//}"
case "$FILE" in
*".env"|*"package-lock.json"|*".git/"*)
echo "Blocked: $FILE 是受保護的檔案,請勿修改" >&2
exit 2
;;
esac
exit 0
設定裡 matcher 填 "Edit|Write",記得 chmod +x。
② 多語言自動格式化(PostToolUse)
#!/bin/bash
FILE=$(jq -r '.tool_input.file_path // .tool_input.file' /dev/stdin)
case "$FILE" in
*.ts|*.tsx|*.js|*.jsx|*.json) npx prettier --write "$FILE";;
*.py) black "$FILE" 2>/dev/null || ruff format "$FILE";;
*.go) gofmt -w "$FILE";;
*.rs) rustfmt "$FILE";;
esac
exit 0 # 格式化失敗不該阻擋 Claude
③ 攔截危險指令(PreToolUse)
#!/bin/bash
COMMAND=$(echo "$(cat)" | jq -r '.tool_input.command')
if echo "$COMMAND" | grep -qiE 'rm -rf /|drop table|git push --force.*main'; then
echo "Blocked: 危險指令被政策攔下:$COMMAND" >&2
exit 2
fi
exit 0
④ 桌面通知(Notification)
# macOS
osascript -e 'display notification "任務完成或需要你授權" with title "Claude Code"'
# Linux
notify-send "Claude Code" "需要你的注意"
⑤ Compact 後重新注入關鍵上下文(SessionStart + matcher compact)
#!/bin/bash
echo '提醒:本專案一律使用 pnpm(不是 npm/yarn);部署走 make deploy;API 金鑰都在 .env.example 有說明。'
純文字 stdout 會直接進 Claude 的 context,compaction 吃掉的脈絡就補回來了。
⑥ 背景跑測試不塞車(PostToolUse + async)
handler 加上 "async": true,測試在背景慢慢跑、Claude 繼續做事,跑完再用 additionalContext 把結果回報進對話。長時間的 build/test 都適用這招。
🚀 搭配閱讀:Hooks 通常和 Skill、MCP 一起組工作流——Skill 管「教 Claude 怎麼做」,MCP 管「給 Claude 新工具」,Hooks 管「強制規矩」。三者關係可以看我們之前的 Claude Code MCP & Skill 完整攻略。
除錯技巧
| 方法 | 用途 |
|---|---|
| /hooks | session 內唯讀瀏覽所有已註冊 hooks,標示來源(User/Project/Local/Plugin settings) |
| claude --debug | debug log 寫到 ~/.claude/debug/<session-id>.txt,看 hook 有沒有被觸發、輸出了什麼 |
| Ctrl+O transcript | 逐條檢視每次 hook 的結果(阻塞訊息/非阻塞錯誤) |
| 手動管線測試 | echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh 先驗腳本再上設定 |
經典坑:「我的 Hook JSON has no effect」
症狀:hook 明明跑了、exit 0、輸出了 JSON,卻完全沒效果。原因通常是 shell-form hook 經 sh -c 啟動時 source 了你的 profile,profile 裡無條件的 echo 混在 JSON 前面,導致 stdout 第一個字元不是 {,整段被當純文字。解法:profile 裡的輸出用 if [[ $- == *i* ]] 包起來(只在互動 shell 印)。其他常見問題:nvm 裝的工具找不到 PATH(改用絕對路徑)、matcher 拼錯、改完 settings 沒被 watcher 抓到(重啟 session)。
安全性:一定要知道的事
- Hooks 以你的完整使用者權限執行。官方 disclaimer 原話:能改、刪、存取你帳號碰得到的任何東西。加任何 hook 前先 review、先測試。
- 小心別人 repo 的
.claude/settings.json。互動模式下,信任對話框接受前所有 hooks 都被扣住;但-p/SDK 模式永不跳對話框、直接視為已信任——惡意 repo 的 hooks 會在你claude -p它的時候直接執行。跑陌生 repo 前:檢查它的.claude/目錄,或用--settings '{"disableAllHooks": true}'。 if過濾器 fail-open。解析不了的 Bash 指令會照常放行——硬性資安政策請靠 permission system,不要只依賴 hook。- 超時不等於擋下。PreToolUse hook 卡死超時會被取消、不做任何決策,照正常權限流程走。卡住的閘門不是閘門。
官方五條最佳實務:驗證消毒所有輸入;shell 變數永遠加引號 "$VAR";防範 .. 路徑穿越;用絕對路徑(exec form 配 ${CLAUDE_PROJECT_DIR});跳過 .env、.git/、金鑰等敏感檔。
常見問題 FAQ
改了 settings.json 要重啟嗎?
通常不用——file watcher 會自動熱載入。若幾秒後還沒生效再重啟 session。用 /hooks 確認目前生效的清單最快。
多個 hook 同時符合會怎樣?
平行執行。決策衝突時 PreToolUse 優先序是 deny > defer > ask > allow;多個 additionalContext 會疊加。避免兩個 hook 同時改 updatedInput(順序不定,最後完成者勝)。
Stop hook 一直擋會不會鬼打牆?
連續擋 8 次後系統會強制結束 turn。腳本開頭先檢查輸入的 stop_hook_active 欄位,true 就直接 exit 0,是最乾淨的防呆。
hook 裡可以自己開終端機或發通知音嗎?
hook process 沒有 controlling terminal,不能開 /dev/tty。要發桌面通知或響鈴,請透過 JSON 輸出的 terminalSequence 欄位代發(白名單限定 OSC 0/9/777 等 escape sequence)。
可以只對特定副檔名的 Edit 觸發嗎?
可以,用 if 欄位:如 "Edit(*.ts)|Write(*.js)",比在腳本裡 case 分派更省事。注意它只對工具類事件有效,掛在其他事件的 hook 加了 if 永遠不會跑。
總結
Hooks 的心智模型很簡單:Claude Code 的生命週期是一條流水線,hooks 就是你在流水線上設的檢查站。SessionStart 發放作業手冊、PreToolUse 安檢把關、PostToolUse 做品質加工、Stop 驗收放行。把「每次都要做、絕不能忘」的規則從 prompt 搬進 hooks,你的 Claude Code 才算從「很有潛力的實習生」晉級成「按 SOP 作業的正規軍」。
上手路徑:先抄自動格式化那一道食譜感受確定性 → 再加敏感檔案保護建立安全感 → 最後玩 Stop 迴圈和 async 測試這些進階招。記得那句話:AI 會忘,hook 不會。
🎯 快速上手:建.claude/hooks/format.sh並chmod +x→.claude/settings.json註冊 PostToolUse +"matcher": "Edit|Write"→ 手動echoJSON 管線測試 → 讓 Claude 改個檔案看 prettier 自動跑。第一個 hook 成功的那一刻,你就回不去了。