🎯 重點摘要: 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.jsonplugin 啟用期間隨 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"、省略代表全部
ifpermission rule 語法的第二層過濾,例如 "Bash(git *)""Edit(*.ts)"
timeout秒數。command/http 型預設 600 秒;prompt 型 30 秒;agent 型 60 秒
asynctrue 時背景執行不阻塞主流程,適合跑測試這類慢活
statusMessage執行中顯示在 spinner 的文字
⚠️ matcher 的坑:含特殊字元(如 .*)的 matcher 會被當成未錨定的 JavaScript regex——Edit.* 會同時命中 EditNotebookEdit!要精確匹配整串請寫 ^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 事件才攔得到。

全部事件一覽表

事件觸發時機可阻擋
SessionStartsession 開始/恢復(startup/resume/clear/compact/fork)否,可注入 context
Setup--init-only 等一次性準備流程
InstructionsLoadedCLAUDE.md 或 rules 檔載入時否,純觀測
UserPromptSubmitprompt 送出、處理前
UserPromptExpansion/指令 展開成 prompt 時
PreToolUse工具執行前✓ 核心攔截點
PermissionRequest權限對話框將彈出前僅 JSON decision
PermissionDeniedauto mode 分類器拒絕工具時僅可要求 retry
PostToolUse工具成功完成後否,stderr 可回饋
PostToolUseFailure工具執行後失敗
PostToolBatch一批平行工具全部結束時✓ 可停住迴圈
Notification發送通知時
MessageDisplay助理訊息串流顯示時(只改畫面不改原文)
SubagentStart / SubagentStopsubagent 生出/完成時Stop 可 ✓
TaskCreated / TaskCompleted任務建立/標記完成時
Stop / StopFailureturn 正常結束/API 錯誤結束Stop 可 ✓
TeammateIdleagent team 隊友即將閒置
ConfigChange設定檔 session 中變更時✓(policy 除外)
CwdChanged / DirectoryAdded工作目錄變更/新增目錄後
FileChanged被 watch 的檔案磁碟變動(誰改的都算)
WorktreeCreate / WorktreeRemoveworktree 建立/移除(可取代預設 git 行為)非零即失敗
PreCompact / PostCompactcontext 壓縮前/後PreCompact 可 ✓
Elicitation / ElicitationResultMCP 請求使用者輸入/回答返回前✓ 可代答或攔截
SessionEndsession 結束(clear/resume/logout/other)

Hook 的輸入:stdin JSON

每個 hook 觸發時都會從 stdin 收到一份 JSON。所有事件都有共同欄位:

欄位說明
session_id目前的 session ID
transcript_path對話 transcript 的路徑(異步寫入可能落後,Stop 類事件建議改用 last_assistant_message)
cwd當下工作目錄
permission_modedefault/plan/acceptEdits/auto/bypassPermissions
hook_event_name事件名稱

工具類事件再加 tool_nametool_inputtool_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 完整攻略

除錯技巧

方法用途
/hookssession 內唯讀瀏覽所有已註冊 hooks,標示來源(User/Project/Local/Plugin settings)
claude --debugdebug 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.shchmod +x.claude/settings.json 註冊 PostToolUse + "matcher": "Edit|Write" → 手動 echo JSON 管線測試 → 讓 Claude 改個檔案看 prettier 自動跑。第一個 hook 成功的那一刻,你就回不去了。