Skip to content

Claude Code Hooks 完整指南:用事件驅動控制 AI 的每一步

2026年3月27日 1 分鐘
TL;DR Hook 是 Claude Code 的事件系統。在 AI 執行工具前後、送出 prompt 時、結束任務時自動觸發 shell command、HTTP 請求、MCP tool 或 LLM 判斷。用來擋住危險操作、自動審核、注入上下文、記錄 audit log。
目錄
  1. 事件生命週期
    1. 完整事件清單
  2. 設定方式
    1. 設定檔位置與優先順序
    2. Matcher 語法
  3. Handler 類型
    1. 1. Command(最常用)
    2. 2. HTTP
    3. 3. Prompt
    4. 4. Agent
  4. 實際應用場景
    1. 場景 1:Commit 前跑 lint + typecheck
    2. 場景 2:擋住危險指令
    3. 場景 3:自動審批安全指令
    4. 場景 4:Stop 攔截——測試沒過不准停
    5. 場景 5:Audit Log
    6. 場景 6:Session 啟動時注入環境變數
    7. 場景 7:Claude 結束時發通知
  5. Hook 的輸入與輸出
    1. PreToolUse 的輸入
    2. PreToolUse 的輸出(可選)
    3. UserPromptSubmit 的輸出
  6. Skill 和 Agent 裡的 Hook
  7. 跟 Skill 的分工
  8. 設計原則
  9. 2026-08 的 hooks 長什麼樣
    1. Handler 從四種變五種
    2. Exit code 的完整語意
    3. Async hooks:背景跑,不擋 loop
    4. HTTP hooks:回應怎麼算
    5. Prompt hooks:讓 LLM 回 ok/false
    6. MCP tool hooks:把判斷外包給已連線的 MCP server
  10. 整體來說
  11. 參考資料
  12. 更新紀錄

🌏 English version

Claude Code 的 Hook 是一套事件驅動系統。在 AI 的操作生命週期中,每個關鍵節點都會發出事件——你可以在這些節點掛上自動執行的動作:擋住危險指令、注入額外上下文、記錄操作日誌、甚至自動審批安全操作。

跟 git hook 或 CI webhook 類似的概念,但作用對象是 AI agent。

事件生命週期

一個 Claude Code session 的事件流長這樣:

SessionStart

UserPromptSubmit(使用者送出訊息)

┌─ Agentic Loop ──────────────────┐
│  PreToolUse → 執行工具 → PostToolUse │
│  PreToolUse → 執行工具 → PostToolUse │
│  ...(重複直到任務完成)              │
└──────────────────────────────────┘

Stop(Claude 結束回應)

SessionEnd

每個事件都可以掛 hook。最常用的是 PreToolUse(工具執行前)和 Stop(任務結束時)。

完整事件清單

事件觸發時機能擋住嗎
SessionStartsession 啟動或恢復
UserPromptSubmit使用者送出 prompt
PreToolUse工具執行前
PostToolUse工具執行後(成功)
PostToolUseFailure工具執行失敗否(已經失敗了)
PermissionRequest權限對話框即將顯示JSON 可自動審批或拒絕(exit 2 不作用)
StopClaude 結束回應能(強制繼續)
StopFailureAPI 錯誤導致結束否(觀察用)
SubagentStart / SubagentStopsubagent 啟動/結束
TaskCreated / TaskCompleted任務建立/完成
Notification通知事件
FileChanged檔案變更
CwdChanged工作目錄變更
ConfigChange設定檔變更
PreCompact / PostCompactcontext 壓縮前後
SessionEndsession 結束否(觀察用)

上表是寫這篇時的核心事件。到 2026-08,官方 reference 的事件已經擴充到三十個上下文:新增了 Setup(CI/腳本的一次性準備)、UserPromptExpansion(slash command 展開時)、PermissionDenied(auto mode 拒絕工具呼叫後)、PostToolBatch(一批平行工具呼叫跑完)、TeammateIdleInstructionsLoadedWorktreeCreateWorktreeRemoveElicitationElicitationResult(MCP elicitation)、DirectoryAddedMessageDisplay 等。完整清單以官方 hooks reference 為準。

設定方式

Hook 定義在 settings.json 裡,三層巢狀結構:事件 → matcher → handler。

// ~/.claude/settings.json(全域)
// 或 .claude/settings.json(專案)
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "./.claude/hooks/check-command.sh"
          }
        ]
      }
    ]
  }
}

設定檔位置與優先順序

位置範圍可共享
~/.claude/settings.json全域(所有專案)
.claude/settings.json單一專案是(commit 到 repo)
.claude/settings.local.json單一專案(個人)否(gitignored)
Managed policy settings組織層級是(管理員控制)
Plugin hooks/hooks.json啟用 plugin 時
Skill/Agent frontmatterSkill:被叫用後的整個 session;Subagent:執行期間

這些設定檔在 .claude 目錄體系裡的位置,.claude 目錄完全導覽有完整說明。

Matcher 語法

matcher 決定 hook 什麼時候觸發,匹配對象是事件帶的欄位(工具事件是工具名稱)。求值規則:只含字母、數字、_-、空白、,| 時按精確字串比對(|, 分隔多個候選,v2.1.191 起支援逗號);含其他字元則當成 JavaScript regex(未錨定)。

"matcher": "Bash"         // 只在 Bash 工具觸發(精確比對)
"matcher": "Edit|Write"   // Edit 或 Write 時觸發
"matcher": "^Edit$"       // regex 寫法;不錨定的 "Edit.*" 會連 NotebookEdit 一起命中
"matcher": "mcp__github__.*" // GitHub MCP server 的所有工具;.* 不能省,
                             // 否則整串被當精確字串、一個都 match 不到
"matcher": ""             // 所有情況都觸發

注意:matcher 只匹配工具名稱,不能"Bash(git commit*)" 這種帶參數的語法——那是 permission rule 語法,要放在 handler 層的 if 欄位(見下方〈2026-08 的 hooks 長什麼樣〉)。

不同事件的 matcher 匹配對象不同:

事件匹配對象範例
PreToolUse / PostToolUse工具名稱BashEditmcp__memory__.*
SessionStart啟動來源startupresumeclearcompactfork
StopFailure錯誤類型rate_limitserver_error
FileChanged檔案名稱.envrcpackage.json
Notification通知類型permission_promptidle_prompt

Handler 類型

handler 現在有五種:command、http、prompt、agent,加上後來新增的 mcp_tool(見下方〈2026-08 的 hooks 長什麼樣〉)。這節先講最常用的前四種。

1. Command(最常用)

執行 shell command,透過 stdin 接收 JSON 輸入,stdout 輸出 JSON 結果。

{
  "type": "command",
  "command": "./.claude/hooks/lint-check.sh",
  "timeout": 600
}

Exit code 決定行為

Exit Code意義行為
0成功、無決定stdout 開頭是 { 就解析成 JSON;不是的話當純文字。不代表放行,工具照走正常 permission flow
2阻擋阻擋該動作;JSON 的 allow 也覆蓋不掉它。訊息優先用 JSON 裡的 reason,否則用 stderr 回饋給 Claude(部分事件只顯示給使用者)
其他非阻擋錯誤不擋。若 stdout 是通過 schema 驗證的 JSON,Claude Code 忽略 exit code、以 JSON 內容為準;否則視為非阻擋錯誤,stderr 只在 debug/verbose 模式可見

細節(含每個事件的差異)見下方〈2026-08 的 hooks 長什麼樣〉的 exit code 一節。

2. HTTP

發 HTTP POST 到指定 endpoint,適合串接外部服務。

{
  "type": "http",
  "url": "http://localhost:8080/hooks/validate",
  "timeout": 30,
  "headers": {
    "Authorization": "Bearer $MY_TOKEN"
  },
  "allowedEnvVars": ["MY_TOKEN"]
}

環境變數需要在 allowedEnvVars 明確列出才會被替換,安全考量。

3. Prompt

用 LLM 判斷。適合需要語意理解的場景(例如判斷指令是否安全)。

{
  "type": "prompt",
  "prompt": "這個操作是否安全?$ARGUMENTS",
  "model": "claude-haiku-4-5",
  "timeout": 30
}

4. Agent

用完整的 agent 處理,有更多工具和上下文。成本最高,適合複雜判斷。

{
  "type": "agent",
  "prompt": "驗證這個條件:$ARGUMENTS",
  "timeout": 60
}

實際應用場景

場景 1:Commit 前跑 lint + typecheck

最基礎的用法,在 Claude 執行 git commit 前自動檢查。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash(git commit*)",
        "hooks": [{
          "type": "command",
          "command": "cd ${CLAUDE_PROJECT_DIR} && pnpm run lint && pnpm run typecheck"
        }]
      }
    ]
  }
}

lint 或 typecheck 失敗 → exit code 非 0 → 阻擋 commit。Claude 會看到錯誤訊息,但 command hook 不能讓 Claude 自動修(那是 Skill 的工作)。

場景 2:擋住危險指令

用腳本解析指令內容,攔截 rm -rfDROP TABLE 等危險操作。

#!/bin/bash
# .claude/hooks/block-dangerous.sh
COMMAND=$(jq -r '.tool_input.command' < /dev/stdin)

if echo "$COMMAND" | grep -qE 'rm -rf|DROP TABLE|--force'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Destructive command blocked by hook"
    }
  }'
else
  exit 0
fi
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "./.claude/hooks/block-dangerous.sh"
      }]
    }]
  }
}

場景 3:自動審批安全指令

每次 Claude 要跑 npm test 都要你點確認很煩。用 hook 自動放行已知安全的指令。

#!/bin/bash
# .claude/hooks/auto-approve.sh
COMMAND=$(jq -r '.tool_input.command' < /dev/stdin)

if [[ "$COMMAND" =~ ^(npm test|pnpm run lint|git status|git log) ]]; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "allow",
      permissionDecisionReason: "Safe read-only command"
    }
  }'
else
  exit 0  # 不判斷,讓正常流程處理
fi

場景 4:Stop 攔截——測試沒過不准停

Claude 說「完成了」但測試沒過?用 Stop hook 強制它繼續。

#!/bin/bash
# .claude/hooks/must-pass-tests.sh
INPUT=$(cat)
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active')

# 防止無限迴圈:如果已經被攔截過一次,放行
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
  exit 0
fi

if ! npm test 2>&1; then
  jq -n '{
    decision: "block",
    reason: "Tests failed. Fix the failures before stopping."
  }'
else
  exit 0
fi

stop_hook_active 是關鍵——第二次觸發時它會是 true,避免 Claude 陷入無限迴圈。Claude Code 自己也有保險:連續攔截 8 次後會強制結束 turn,不讓 hook 把 session 鎖死。

場景 5:Audit Log

所有 Claude 的操作都記錄下來,用於事後稽核。

#!/bin/bash
INPUT=$(cat)
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')

echo "{\"ts\": \"$TIMESTAMP\", \"event\": \"$EVENT\", \"tool\": \"$TOOL\"}" \
  >> ~/.claude/audit.log
exit 0

搭配 "async": true 跑在背景,不影響 Claude 的回應速度。

場景 6:Session 啟動時注入環境變數

#!/bin/bash
# SessionStart hook:載入 .envrc
if [ -n "$CLAUDE_ENV_FILE" ] && [ -f .envrc ]; then
  eval "$(direnv export bash)"
  direnv export bash >> "$CLAUDE_ENV_FILE"
fi
exit 0

CLAUDE_ENV_FILE 是 Claude Code 提供的特殊變數。寫入這個檔案的環境變數會在整個 session 中生效。

場景 7:Claude 結束時發通知

{
  "hooks": {
    "Stop": [{
      "matcher": "",
      "hooks": [{
        "type": "command",
        "command": "osascript -e 'display notification \"Done\" with title \"Claude Code\"'"
      }]
    }]
  }
}

Hook 的輸入與輸出

每個 hook 透過 stdin(command)或 POST body(HTTP)收到 JSON,格式因事件而異。

PreToolUse 的輸入

{
  "session_id": "abc123",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test",
    "description": "Run tests"
  }
}

PreToolUse 的輸出(可選)

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "Safe command",
    "updatedInput": {
      "command": "npm test -- --verbose"
    },
    "additionalContext": "This project uses Jest"
  }
}

注意 updatedInput——你可以修改 Claude 即將執行的工具輸入。例如自動加上 --verbose flag,或把相對路徑改成絕對路徑。

UserPromptSubmit 的輸出

{
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "Current branch: main, last commit: abc1234"
  }
}

additionalContext 會被注入到 Claude 的上下文中。可以用來在每次使用者送 prompt 時自動補充資訊(例如 git 狀態、目前 branch)。

Skill 和 Agent 裡的 Hook

Hook 不只能定義在 settings.json。Skill 和 Agent 的 frontmatter 裡也能定義,作用範圍限定在元件的生命週期內。

---
name: secure-operations
description: 執行需要安全檢查的操作
hooks:
  PreToolUse:
    - matcher: "Bash"
      hooks:
        - type: command
          command: "./scripts/security-check.sh"
---

這個 hook 在 secure-operations skill 被叫用後註冊,之後整個 session 都有效(不只 skill 自己那輪)。想只跑一次,在 handler 上加 once: true。Subagent frontmatter 裡的 hook 則只在該 subagent 執行期間生效。

跟 Skill 的分工

這個問題在三件套協作視角:Hook、Skill、指令檔裡有詳細討論,Skill 本身怎麼設計則見Skills 主篇,這裡簡要重提:

特性HookSkill
觸發方式自動(事件驅動)手動(/name)或指令檔指示
能力shell command / HTTP / LLMClaude 的完整能力
能修 code 嗎不能
能互動嗎不能(除了 prompt type)
適合場景擋、記錄、注入修、生成、互動

Hook 是被動安全網,Skill 是主動工作流程。 Hook 負責「不讓壞事發生」,Skill 負責「把事情做好」。

設計原則

Hook 要快。 特別是 SessionStartPreToolUse,每次都會跑。如果 hook 很慢,Claude 的回應也會變慢。重度操作用 "async": true 跑在背景。

不要在 stderr 暴露敏感資訊。 Exit code 2 的 stderr 會被回饋給 Claude。如果你的檢查腳本涉及 API key 或內部路徑,要注意不要讓這些出現在 stderr。

stop_hook_active 防止無限迴圈。 Stop hook 攔截 Claude 後,Claude 會繼續工作然後再次觸發 Stop。如果不檢查 stop_hook_active,就會無限循環。

Command hook 的 stdout 只能是 JSON。 Shell profile 印出的歡迎訊息、echo debug 訊息都會破壞 JSON 解析。確保 stdout 只有你的 JSON output。

Managed policy hook 不能被覆蓋。 組織管理員透過 policy settings 設定的 hook,使用者和專案層級都無法停用。這是企業安全的保證。

2026-08 的 hooks 長什麼樣

這篇發佈後 hooks 又長大了一輪。以下是依官方 hooks reference 補的增量,沒講到的不推導。

Handler 從四種變五種

type 現在有 commandhttpmcp_toolpromptagent 五種。所有 handler 共用這幾個欄位:

欄位說明
ifpermission rule 語法的過濾條件(如 "Bash(git *)""Edit(*.ts)"),只在工具事件上生效。只接受單一規則,沒有 && 或串列語法
timeout秒數。command/http/mcp_tool 預設 600,prompt 30,agent 60
statusMessagehook 執行中顯示的 spinner 訊息
oncetrue 時每個 session 只跑一次(只在 skill frontmatter 裡有效)

Command hook 另外支援 args(設了就跳過 shell、直接 spawn 執行檔的 exec form)和 shell"bash""powershell")。同一事件的所有 matching hooks 平行執行;多個 hook 都回決定時取最嚴格的(deny > defer > ask > allow),additionalContext 則全部保留。

Exit code 的完整語意

Exit Code行為
0成功。stdout 首字非 { 就當純文字——UserPromptSubmitUserPromptExpansionSessionStart 三個事件的純文字 stdout 會進 Claude 的 context,其他事件只進 debug log
2阻擋錯誤。唯一能靠 exit code 本身擋下動作的代碼,JSON 輸出覆蓋不掉。但「擋」的效果因事件而異:PreToolUse 擋工具呼叫、Stop 強制繼續、ConfigChange 擋設定變更生效;而 PermissionRequest 根本不吃 exit 2(要用 JSON 的 decision.behavior)、PostToolUse 只把 stderr 給 Claude 看(工具已經跑完了)、觀察類事件(SessionEndNotification 等)完全忽略
其他非阻擋錯誤。例外:stdout 是通過 schema 驗證的 JSON 時,exit code 被忽略、以 JSON 為準。所以想靠 hook 擋東西只能用 exit 2——慣例上的 exit 1 在多數事件上只是非阻擋錯誤,動作照樣執行

另一個容易踩的坑:超時的 hook 直接被取消、不做任何決定——在 PreToolUse 上,卡死的 hook 不會充當關卡,工具呼叫照走正常 permission flow。

Async hooks:背景跑,不擋 loop

"async": true 加在 command hook 上,Claude Code 啟動程序後立刻繼續工作:

  • 背景跑完後,輸出的 additionalContextsystemMessage下一輪對話才交付給 Claude;session 閒置時就等到下次互動
  • 既然動作已經發生,async hook 的 decisionpermissionDecisioncontinue 全部無效——它天生不能拿來擋東西
  • 想要背景跑、但失敗時要叫醒 Claude,改用 "asyncRewake": true:exit 2 時立即以 system reminder 餵給 Claude(含 session 閒置中)
  • 每次觸發都是獨立的背景程序,同一個 async hook 連續觸發不會去重

HTTP hooks:回應怎麼算

HTTP hook 把事件的 JSON 當 POST body 送出,回應的算法和 command hook 不同:

  • 2xx + JSON object:按 command hook 的 JSON output 同一套 schema 解析——要擋就回 2xx 帶 decision 欄位
  • 2xx + 空 body:等同 exit 0 無輸出
  • 非 2xx、連線失敗:非阻擋錯誤,繼續執行
  • 超時:取消、不做決定

重點:HTTP hook 無法用 status code 表達阻擋,純文字回應也進不了 Claude 的 context。

Prompt hooks:讓 LLM 回 ok/false

Prompt hook 是單輪 LLM 判斷(預設 fast model)。模型必須回這個 JSON:

{
  "ok": true,
  "reason": "說明",
  "impossible": false
}

ok: false 的效果因事件而異:StopSubagentStop 把 reason 餵回給 Claude 繼續做(除非模型同時判斷 impossible: true,那就放行結束);PreToolUse 預設直接結束 turn,設 continueOnBlock: true 才會把 reason 當 tool error 讓 Claude 調整後繼續。需要更細的控制(allow/deny/ask、改參數)還是用 command hook 的 JSON 輸出。

MCP tool hooks:把判斷外包給已連線的 MCP server

{
  "type": "mcp_tool",
  "server": "my_server",
  "tool": "security_scan",
  "input": { "file_path": "${tool_input.file_path}" }
}

呼叫已連線 MCP server 上的工具,tool 的文字輸出按 command hook stdout 的同一套規則解析。input 字串支援 ${path} 語法從事件的 JSON 抽欄位。server 必須已經連著——hook 不會觸發 OAuth 或連線流程;沒連上或 tool 回 isError: true 就是非阻擋錯誤。SessionStartSetup 通常比 MCP 連線先觸發,掛在這兩個事件上第一次會吃到「not connected」。

另外不是每個事件都吃五種 handler:工具與 turn 類事件(PreToolUseStopUserPromptSubmit 等十三個)五種全收;觀察類事件(NotificationFileChanged 等)不支援 prompt/agent;SessionStartSetup 只收 command 和 mcp_tool。

整體來說

Hook 是 Claude Code 最底層的控制機制。它不聰明(不會理解程式碼),但很可靠(機制層面保證執行)——本質上就是在 agentic loop 的特定時點插上你的動作,系列入口篇對那個 loop 有完整拆解。

大多數人只需要兩三個 hook:commit 前跑檢查、擋住危險指令、結束時發通知。從這些開始,遇到具體需求再加。不要過度設計——能用簡單的 exit code 解決的事,不需要動用 prompt 或 agent type。

最強大的用法是跟 Skill 組合。Hook 擋住問題,Skill 修復問題,指令檔串起流程。三層各司其職,AI 就有了一套完整的品質保證系統。


參考資料

更新紀錄

  • 2026-08-26:依 hooks reference 補 schema/exit code/async/HTTP/prompt/MCP tool hooks 增量。