Claude Code Hooks 實戰:讓 AI 工作流自動化的最後一塊拼圖


這是 Claude Code 系列的第六篇。如果你還沒看過前幾篇: 📖 Claude Code 權限控管:用 settings.local.json 保護你的專案 📖 Claude Code 自訂 Slash Commands:打造你的專屬 AI 工作流


🎯 前言

在前幾篇文章中,我們用 settings.local.json 設定了權限控管,用 Custom Slash Commands 定義了工作流程。但有一件事還是做不到:

在 Claude 執行特定動作的「當下」自動觸發一段腳本。

舉個例子:

  • Claude 每次編輯完檔案,你想自動跑 prettier 格式化
  • Claude 要刪除檔案時,你想先檢查是不是重要檔案再決定放不放行
  • Claude 結束回答時,你想自動跑測試確認沒有改壞東西

這些需求,settings.local.json 做不到(它只管「能不能用」),Slash Commands 也做不到(它只是 prompt)。

Hooks 就是填補這個空缺的機制。

它是 Claude Code 的事件系統——在特定時間點觸發你寫的腳本,而且是確定性的:不像 prompt 會被 Claude 自由詮釋,hook 腳本每次都會照樣執行。


📂 Hooks 的基本概念

和權限設定的差異

先釐清 Hooks 和 settings.local.json 的權限設定有什麼不同:

面向權限設定(permissions)Hooks
功能控制「能不能用」某個工具在工具執行前後「做一件事」
行為允許 / 拒絕 / 詢問執行腳本、發 HTTP、跑 LLM 判斷
彈性只能 allow / deny可以修改輸入、注入上下文、阻擋執行
時機工具被呼叫時26 種不同事件時間點

簡單說:權限設定是「門禁」,Hooks 是「監控攝影機 + 自動化閘門」。

四種 Hook 類型

Hooks 不只能跑 shell 腳本,共有四種類型:

類型說明適用場景
command執行 shell 指令跑 linter、格式化、檢查檔案
httpPOST 事件 JSON 到指定 URL發通知到 Slack、寫 log 到外部服務
prompt單輪 LLM 判斷(回傳 ok/reason)用 AI 檢查 commit message 品質
agent多輪 subagent,可使用工具自動跑測試、驗證任務完成度

大多數情況用 command 就夠了。promptagent 是進階用法,適合需要 AI 判斷的場景。


⚙️ 設定語法

Hooks 寫在 settings.json 中,和權限設定放同一個檔案:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python check_command.py"
          }
        ]
      }
    ]
  }
}

結構是:事件名稱 → 匹配規則 → 要執行的 hook 列表

設定檔位置(和權限設定相同)

位置影響範圍
~/.claude/settings.json全域(所有專案)
.claude/settings.json單一專案(提交到 git)
.claude/settings.local.json單一專案(不提交 git)

matcher:決定什麼時候觸發

matcher 是正規表達式,用來過濾工具名稱:

"matcher": "Bash"              // 只匹配 Bash 工具
"matcher": "Edit|Write"        // 匹配 Edit 或 Write
"matcher": "mcp__.*"           // 所有 MCP 工具
"matcher": ""                  // 匹配所有(或省略 matcher)

如果需要更精細的過濾(例如只匹配 git 開頭的 Bash 指令),可以用 if 欄位:

{
  "matcher": "Bash",
  "hooks": [
    {
      "type": "command",
      "command": "echo 'git command detected'",
      "if": "Bash(git *)"
    }
  ]
}

if 使用的是和權限設定相同的語法(工具名(參數模式)),比 matcher 的正規表達式更直覺。


🔑 關鍵事件類型

Hooks 支援 26 種事件,但日常最常用的是這幾個:

PreToolUse(工具執行前)

最重要的 hook。 在 Claude 呼叫任何工具之前觸發,你可以:

  • 檢查並阻擋危險操作(exit code 2)
  • 修改工具的輸入參數
  • 注入額外的上下文給 Claude
  • 自動允許特定操作(跳過權限確認)
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python scripts/check_dangerous_commands.py"
          }
        ]
      }
    ]
  }
}

PostToolUse(工具執行後)

工具成功執行之後觸發。注意:此時動作已經完成,無法復原,但你可以:

  • 對修改過的檔案跑格式化
  • 記錄操作日誌
  • 阻擋 Claude 繼續使用這個結果(decision: "block"
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write \"$CHANGED_FILE\""
          }
        ]
      }
    ]
  }
}

Stop(Claude 結束回答時)

Claude 完成回答、準備停下來時觸發。你可以:

  • 驗證任務是否真的完成
  • 自動跑測試
  • 阻擋停止,讓 Claude 繼續工作
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "npm test 2>&1 | tail -5"
          }
        ]
      }
    ]
  }
}

其他實用事件

事件用途
SessionStartSession 開始時注入提醒(例如 compact 後重新注入規則)
NotificationClaude 需要你注意時發桌面通知
UserPromptSubmit用戶送出 prompt 前做預處理
FileChanged監控檔案變更(例如 .env 被修改時重載環境變數)

📊 Exit Code 行為

Hook 腳本的 exit code 決定了後續行為:

Exit Code效果
0成功,動作繼續。stdout 會被解析為 JSON 輸出
2阻擋。動作被取消,stderr 會回饋給 Claude
其他非阻擋錯誤。動作繼續,stderr 只在 verbose 模式顯示

這個設計很聰明:只有 exit 2 會阻擋操作。腳本自身出錯(exit 1)不會影響 Claude 的正常運作,避免你的 hook 寫壞了導致整個 Claude Code 卡住。


🛡️ 實戰範例

範例 1:阻擋危險的 Bash 指令

最基本的安全防護——在 Claude 執行 Bash 前檢查是否包含危險指令:

scripts/block_dangerous.sh

#!/bin/bash
# 從 stdin 讀取事件 JSON
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')

# 定義危險模式
DANGEROUS_PATTERNS=(
  "rm -rf"
  "drop table"
  "DROP TABLE"
  "truncate"
  "TRUNCATE"
  "> /dev/null"
  "format"
  "mkfs"
)

for pattern in "${DANGEROUS_PATTERNS[@]}"; do
  if echo "$COMMAND" | grep -qi "$pattern"; then
    echo "⚠️ 偵測到危險指令: $COMMAND" >&2
    echo "包含危險模式: $pattern" >&2
    exit 2  # 阻擋執行
  fi
done

exit 0  # 允許執行

settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash scripts/block_dangerous.sh"
          }
        ]
      }
    ]
  }
}

當 Claude 嘗試執行 rm -rf /some/path 時,hook 會阻擋並告訴 Claude 原因,Claude 會換一個更安全的方式。

範例 2:編輯後自動格式化

每次 Claude 修改檔案後,自動跑 Prettier:

scripts/auto_format.sh

#!/bin/bash
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=""

if [ "$TOOL_NAME" = "Edit" ]; then
  FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
elif [ "$TOOL_NAME" = "Write" ]; then
  FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
fi

if [ -n "$FILE_PATH" ] && [ -f "$FILE_PATH" ]; then
  # 只格式化支援的檔案類型
  case "$FILE_PATH" in
    *.js|*.ts|*.jsx|*.tsx|*.json|*.css|*.md|*.html)
      npx prettier --write "$FILE_PATH" 2>/dev/null
      ;;
  esac
fi

exit 0

settings.json

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "bash scripts/auto_format.sh"
          }
        ]
      }
    ]
  }
}

範例 3:保護重要檔案不被修改

防止 Claude 修改 .envpackage-lock.json 等不該手動編輯的檔案:

#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')

PROTECTED_FILES=(
  ".env"
  ".env.local"
  "package-lock.json"
  "pnpm-lock.yaml"
  "yarn.lock"
)

BASENAME=$(basename "$FILE_PATH")
for protected in "${PROTECTED_FILES[@]}"; do
  if [ "$BASENAME" = "$protected" ]; then
    echo "⛔ 此檔案受保護,不允許修改: $BASENAME" >&2
    echo "如果確實需要修改,請手動操作" >&2
    exit 2
  fi
done

exit 0
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "bash scripts/protect_files.sh"
          }
        ]
      }
    ]
  }
}

範例 4:桌面通知(Claude 需要你注意時)

Claude 在等你回應或需要權限確認時,發一個系統通知:

Windows(PowerShell)

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "powershell -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code 需要你的注意', 'Claude Code')\"",
            "shell": "powershell"
          }
        ]
      }
    ]
  }
}

macOS

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code 需要你的注意\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

範例 5:用 AI 驗證任務完成度(進階)

這是最強大的用法——用 agent 類型的 hook,在 Claude 停下來時自動派一個 subagent 跑測試:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "agent",
            "prompt": "檢查最近修改的檔案是否有對應的測試。如果有測試,執行測試並確認通過。如果測試失敗或缺少測試,回傳 {\"ok\": false, \"reason\": \"測試失敗或缺少測試\"}。如果一切正常,回傳 {\"ok\": true}。",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

如果 subagent 判斷測試沒通過,Claude 會被阻止停止,繼續修復問題。


💡 Hook 腳本接收的資料

每個 hook 腳本會從 stdin 收到一段 JSON,包含事件的完整資訊:

{
  "session_id": "abc123",
  "cwd": "/path/to/project",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test",
    "timeout": 120000
  }
}

你可以用 jq 解析需要的欄位:

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')

PreToolUse 的 JSON 輸出(進階控制)

如果你的 hook 腳本輸出 JSON 到 stdout(exit 0),可以做更精細的控制:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "此操作不允許",
    "updatedInput": {
      "command": "修改後的指令"
    },
    "additionalContext": "注入給 Claude 的額外上下文"
  }
}
欄位說明
permissionDecisionallow(自動允許)、deny(阻擋)、ask(詢問用戶)
updatedInput修改工具的輸入參數
additionalContext注入額外上下文給 Claude

⚠️ 注意事項與陷阱

1. Stop hook 的無窮迴圈

如果你的 Stop hook 阻擋了 Claude 停止,Claude 會繼續工作,完成後又觸發 Stop hook……無限循環。

解法:檢查事件中的 stop_hook_active 欄位:

INPUT=$(cat)
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')

if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
  # 已經在 stop hook 中了,不要再阻擋
  exit 0
fi

# 正常的檢查邏輯...

2. Shell profile 污染 JSON 輸出

如果你的 ~/.bashrc~/.zshrcecho 語句,它們會混入 hook 的 stdout,破壞 JSON 解析。

解法:在 shell profile 中加上互動模式判斷:

# ~/.bashrc
if [[ $- == *i* ]]; then
  echo "Welcome!"  # 只在互動模式才執行
fi

3. PostToolUse 無法復原

PostToolUse 在工具已經執行完之後才觸發。你可以阻擋 Claude 繼續使用結果,但動作本身無法撤銷。

如果要阻擋操作,用 PreToolUse。

4. 多個 hook 的衝突解決

當同一個事件有多個 hook 同時觸發,Claude Code 取最嚴格的結果。只要有一個 hook 回傳 deny,操作就會被取消,不管其他 hook 怎麼說。

5. 預設 timeout

類型預設 timeout
command600 秒(10 分鐘)
http30 秒
prompt30 秒
agent60 秒

如果你的腳本可能跑比較久(例如跑完整測試),記得設定 timeout


📋 我的實際設定

結合前幾篇文章的設定,我在 AppSystem 專案中的 .claude/settings.local.json

{
  "permissions": {
    "allow": [
      "Bash(dir:*)"
    ]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash scripts/block_dangerous.sh",
            "statusMessage": "正在檢查指令安全性..."
          }
        ]
      },
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "bash scripts/protect_files.sh",
            "statusMessage": "正在檢查檔案保護規則..."
          }
        ]
      }
    ],
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "powershell -Command \"Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('Claude Code 需要你的注意', 'Claude Code')\"",
            "shell": "powershell"
          }
        ]
      }
    ]
  }
}

這個設定搭配權限控管一起使用:

  • 權限層:只有 dir 可以自動執行,其他 Bash 指令都要手動確認
  • Hook 層:即使手動確認了,還是會檢查是否包含危險指令

兩層防護,各司其職。


🎉 結語

Hooks 是 Claude Code 自動化的最後一塊拼圖。

機制解決什麼問題
CLAUDE.mdClaude 怎麼理解你的專案
settings.local.jsonClaude 能用哪些工具
Custom Slash CommandsClaude 怎麼執行工作流程
Hooks在 Claude 動作的當下自動做一件事

建議的入門順序:

  1. 先加 Notification hook — 最簡單,立刻有感,Claude 需要你時會通知你
  2. 再加 PreToolUse 保護 — 阻擋危險指令和受保護檔案
  3. 最後嘗試 PostToolUse 自動化 — 自動格式化、記錄日誌

不需要一次全加。先從一個你覺得最痛的問題開始,跑幾天看效果,再慢慢擴充。


📎 相關文章