精選
Hooks Automation Configuration Development
Claude Code Hooks 開發指南:完整實戰手冊
從輸入輸出規範、腳本模板到常見錯誤解決方案,全面掌握 Claude Code Hooks 開發技巧。
2026年1月17日 • 20 分鐘閱讀 • 作者:Claude World
Hooks 是 Claude Code 的強大擴展機制,讓你能在工具執行前後注入自訂邏輯。本指南整理了實戰經驗,幫助你避開常見陷阱,快速上手 Hooks 開發。
Hook 類型總覽
| Hook 類型 | 觸發時機 | 用途 |
|---|---|---|
| PreToolUse | 工具執行前 | 阻擋危險操作、添加上下文 |
| PostToolUse | 工具執行後 | 記錄操作、後處理 |
| SessionStart | Session 啟動時 | 載入環境、顯示資訊 |
| 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+ 實測,行為可能隨版本更新而改變。