跳至主要內容
精選 Hooks Automation Configuration Development

Claude Code Hooks 開發指南:完整實戰手冊

從輸入輸出規範、腳本模板到常見錯誤解決方案,全面掌握 Claude Code Hooks 開發技巧。

2026年1月17日 20 分鐘閱讀 作者:Claude World

Hooks 是 Claude Code 的強大擴展機制,讓你能在工具執行前後注入自訂邏輯。本指南整理了實戰經驗,幫助你避開常見陷阱,快速上手 Hooks 開發。


Hook 類型總覽

Hook 類型觸發時機用途
PreToolUse工具執行前阻擋危險操作、添加上下文
PostToolUse工具執行後記錄操作、後處理
SessionStartSession 啟動時載入環境、顯示資訊
Stop自動化循環結束時決定是否繼續循環

輸入輸出規範

輸入格式(stdin JSON)

所有 Hook 都會收到 JSON 格式的輸入:

{
  "tool_name": "Bash",
  "tool_input": {
    "command": "ls -la",
    "file_path": "/path/to/file",
    "content": "file content..."
  },
  "tool_response": "command output..."
}

常用欄位:

  • tool_name - 工具名稱(Bash, Write, Edit, Read 等)
  • tool_input.command - Bash 命令
  • tool_input.file_path - 檔案路徑
  • tool_input.content - Write 的檔案內容
  • tool_input.new_string - Edit 的新內容
  • tool_response - 工具執行結果(PostToolUse)

輸出格式

1. 允許操作(無輸出)

exit 0

2. 阻擋操作(exit 2 + stderr)

echo "🛡️ BLOCKED: 原因說明" >&2
exit 2

3. 添加上下文(PreToolUse)

jq -n --arg ctx "上下文訊息" '{
    "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "additionalContext": $ctx
    }
}'
exit 0

4. Stop Hook 阻止停止

jq -n --arg reason "繼續原因" '{
    "decision": "block",
    "reason": $reason
}'
exit 0

⚠️ 重要:舊格式不再支援

# ❌ 錯誤(會導致 hook error)
echo '{"decision": "allow"}'

# ✅ 正確
exit 0

腳本模板

PreToolUse - 阻擋型(Security Guard)

#!/bin/bash
# Security Guard - 阻擋危險命令
# PreToolUse hook for Bash tool

# 讀取輸入
HOOK_INPUT=$(cat)
COMMAND=$(echo "$HOOK_INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || echo "")

# 無命令則跳過
[[ -z "$COMMAND" ]] && exit 0

# 檢查危險模式
# 1. 刪除根目錄
if echo "$COMMAND" | grep -qE 'rm\s+-[rf]+\s+/($|[^a-zA-Z])' 2>/dev/null; then
    echo "🛡️ BLOCKED: Delete root filesystem (rm -rf /)" >&2
    exit 2
fi

# 2. Pipe 遠端腳本到 shell
if echo "$COMMAND" | grep -qE '(curl|wget).*\|\s*(sh|bash)' 2>/dev/null; then
    echo "🛡️ BLOCKED: Pipe remote script to shell" >&2
    exit 2
fi

# 3. Force push 到 main/master
if echo "$COMMAND" | grep -qE 'git\s+push.*--force.*\s+(main|master)' 2>/dev/null; then
    echo "🛡️ BLOCKED: Force push to main/master" >&2
    exit 2
fi

# 允許執行
exit 0

PreToolUse - 上下文型(Git Context)

#!/bin/bash
# Git Context - 添加 Git 狀態上下文
# PreToolUse hook for Bash tool

# 消耗 stdin(重要!即使不需要也要消耗)
cat > /dev/null

# 檢查是否在 git repo
if ! git rev-parse --git-dir &>/dev/null; then
    exit 0
fi

# 收集 Git 資訊
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
UNCOMMITTED=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')

# 輸出上下文
jq -n --arg ctx "[Git] Branch: $BRANCH, Uncommitted: $UNCOMMITTED files" '{
    "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "additionalContext": $ctx
    }
}'
exit 0

PreToolUse - 警告型(File Guard)

#!/bin/bash
# File Guard - 警告敏感檔案
# PreToolUse hook for Write/Edit tools

HOOK_INPUT=$(cat)
FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || echo "")

[[ -z "$FILE_PATH" ]] && exit 0

FILENAME=$(basename "$FILE_PATH")
WARNINGS=()

# 檢查敏感模式
if echo "$FILENAME" | grep -qiE '^\.env$|^\.env\.' 2>/dev/null; then
    WARNINGS+=("Environment file may contain secrets")
fi

if echo "$FILENAME" | grep -qiE '\.pem$|\.key$|^id_rsa' 2>/dev/null; then
    WARNINGS+=("File may be a private key")
fi

# 輸出警告
if [[ ${#WARNINGS[@]} -gt 0 ]]; then
    WARNING_TEXT=$(printf '%s; ' "${WARNINGS[@]}")
    jq -n --arg warn "⚠️ Sensitive file ($FILENAME): $WARNING_TEXT" '{
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "additionalContext": $warn
        }
    }'
fi

exit 0

PostToolUse - 記錄型

#!/bin/bash
# Log File Change - 記錄檔案變更
# PostToolUse hook for Write/Edit tools

HOOK_INPUT=$(cat)
TOOL_NAME=$(echo "$HOOK_INPUT" | jq -r '.tool_name // empty' 2>/dev/null || echo "")
FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || echo "")

[[ -z "$TOOL_NAME" ]] && exit 0
[[ -z "$FILE_PATH" ]] && exit 0

# 確保日誌目錄存在
mkdir -p .claude/logs 2>/dev/null

# 記錄變更
case "$TOOL_NAME" in
    Write)
        echo "[$(date -Iseconds)] WRITE: $FILE_PATH" >> .claude/logs/file-changes.log
        ;;
    Edit)
        echo "[$(date -Iseconds)] EDIT: $FILE_PATH" >> .claude/logs/file-changes.log
        ;;
esac

exit 0

Stop Hook - 循環控制

#!/bin/bash
# Post Auto Check - 控制自動化循環
# Stop hook

AUTO_DIR=".auto"
AC_FILE="$AUTO_DIR/acceptance-criteria.json"
STOP_FILE="$AUTO_DIR/stop"

# 無狀態目錄,允許停止
if [ ! -d "$AUTO_DIR" ]; then
    exit 0
fi

# 檢查手動停止信號
if [ -f "$STOP_FILE" ]; then
    rm -rf "$AUTO_DIR"
    exit 0
fi

# 檢查 AC 完成率
if [ -f "$AC_FILE" ] && command -v jq &> /dev/null; then
    PASSED=$(jq '[.criteria[] | select(.status=="passed")] | length' "$AC_FILE" 2>/dev/null || echo "0")
    TOTAL=$(jq '.criteria | length' "$AC_FILE" 2>/dev/null || echo "0")

    if [ "$TOTAL" -gt 0 ] && [ "$PASSED" -lt "$TOTAL" ]; then
        # 阻止停止,繼續循環
        jq -n --arg reason "AC 完成率: $PASSED/$TOTAL,繼續執行" '{
            "decision": "block",
            "reason": $reason
        }'
        exit 0
    fi
fi

# 允許停止
exit 0

設計模式

全域共用 + 命名空間

將 hooks 安裝在用戶層,所有專案共用:

~/.claude/hooks/bootstrap-kit/
├── pre-tool-use/
│   ├── security-guard.sh
│   ├── git-context.sh
│   ├── file-guard.sh
│   └── architecture-guard.sh
├── log-bash-event.sh
├── log-file-change.sh
├── session-start/
│   └── session-start.sh
└── auto/
    └── post_auto_check.sh

settings.json 配置

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/Users/username/.claude/hooks/bootstrap-kit/pre-tool-use/security-guard.sh"
          }
        ]
      },
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "/Users/username/.claude/hooks/bootstrap-kit/pre-tool-use/file-guard.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/Users/username/.claude/hooks/bootstrap-kit/log-bash-event.sh"
          }
        ]
      }
    ],
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "/Users/username/.claude/hooks/bootstrap-kit/session-start/session-start.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": ".*",
        "hooks": [
          {
            "type": "command",
            "command": "/Users/username/.claude/hooks/bootstrap-kit/auto/post_auto_check.sh"
          }
        ]
      }
    ]
  }
}

常見錯誤與解決方案

1. Hook Error 持續出現

症狀

PreToolUse:Bash hook error: [/path/to/hook.sh]: ...

原因:使用舊格式 {"decision": "allow"}

解決:改用 exit 0(無輸出)

2. 路徑無法載入

症狀:Hook 不執行

原因:settings.json 中使用 $HOME${HOME}

解決:使用絕對路徑

// ❌ 錯誤
"command": "$HOME/.claude/hooks/my-hook.sh"

// ✅ 正確
"command": "/Users/username/.claude/hooks/my-hook.sh"

3. Hooks 互相覆蓋

症狀:設定的 hooks 不生效

原因:多個 settings 檔案有衝突配置

解決:檢查並清除以下檔案中的 hooks:

  • ~/.claude/settings.local.json
  • .claude/settings.local.json

4. stdin 未消耗導致錯誤

症狀:Broken pipe 或 hook 異常

原因:腳本未讀取 stdin

解決:即使不需要輸入,也要消耗 stdin

cat > /dev/null
# 或
HOOK_INPUT=$(cat)

5. JSON 輸出格式錯誤

症狀:上下文未顯示或 hook error

原因:JSON 結構不正確

解決:使用 jq 生成 JSON

# ❌ 手動拼接容易出錯
echo '{"hookSpecificOutput":{"additionalContext":"msg"}}'

# ✅ 使用 jq
jq -n --arg ctx "msg" '{
    "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "additionalContext": $ctx
    }
}'

測試方法

1. 直接執行測試

# 測試腳本語法
bash -n /path/to/hook.sh

# 模擬輸入測試
echo '{"tool_input":{"command":"ls"}}' | /path/to/hook.sh

2. 在 Claude Code 中測試

# 測試 PreToolUse (Bash)
echo "test command"

# 測試 PreToolUse (Write) - 觀察是否有警告
# 寫入一個 .env 檔案

# 測試 Security Guard - 應該被阻擋
rm -rf /

3. 檢查日誌

# 檢查檔案變更日誌
cat .claude/logs/file-changes.log

# 檢查事件日誌
cat .claude/logs/events.log

最佳實踐

1. 永遠消耗 stdin

HOOK_INPUT=$(cat)
# 或
cat > /dev/null

2. 使用 jq 處理 JSON

COMMAND=$(echo "$HOOK_INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || echo "")

3. 防禦性編程

# 檢查空值
[[ -z "$COMMAND" ]] && exit 0

# 錯誤處理
command -v jq &> /dev/null || exit 0

4. stderr 用於阻擋訊息

echo "🛡️ BLOCKED: reason" >&2
exit 2

5. 使用絕對路徑

"command": "/Users/username/.claude/hooks/..."

6. 腳本權限

chmod +x /path/to/hook.sh

7. 日誌輸出到 stderr

# 調試訊息
echo "[DEBUG] something" >&2

參考資料


本指南基於 Claude Code v2.1.9+ 實測,行為可能隨版本更新而改變。