Claude Code Hooks 實戰:8 個讓 AI 不再闖禍的自動化規則
CLAUDE.md 是建議,Hooks 是法律。8 個可直接複製的自動化規則,讓 Claude Code 不再 git add .、不看 diff 就 commit、重試到死。附完整 settings.json 範例。
你有沒有過這種經驗:讓 Claude Code 幫你改 code,它順手把 .env 也改了。或是它很熱心地 git add . 把整個目錄都 stage 了,包括你的 credentials。又或者它 commit 了一個你根本沒看過 diff 的變更。
你在 CLAUDE.md 裡寫了「不要碰 .env」「不要 git add .」「commit 前先 diff」,但 CLAUDE.md 只是建議 — Claude 讀了,大概 80% 的時間會遵守。剩下 20% 就是你的 debug 時間。
Hooks 不一樣。它們是硬性的自動化規則 — 每當 Claude 要執行命令、編輯檔案或結束任務時,自動觸發。 不是「請你不要」,是「你不能」。
這篇文章分享我實際在用的 8 個 Hook,每個都附上可以直接複製的程式碼。從今天開始,讓你的 Claude Code 自己守規矩。
Hook 是什麼?30 秒懂
Hook 是 Claude Code 在特定生命週期事件觸發的 shell script。你在 ~/.claude/settings.json(全域)或專案的 .claude/settings.json(專案級)裡設定,Claude 就會在對應時機自動執行。
主要的生命週期事件:
| 事件 | 觸發時機 | 常見用途 |
|---|---|---|
PreToolUse |
Claude 要呼叫工具之前 | 攔截危險命令、改寫指令 |
PostToolUse |
工具執行完畢之後 | 語法檢查、審計日誌 |
FileChanged |
檔案被修改後 | 自動 lint |
SessionStart |
Session 開始時 | 載入環境、設定狀態 |
Stop |
Claude 停下來時 | 自動學習、提醒收尾 |
UserPromptSubmit |
使用者送出 prompt 時 | 關鍵字偵測、scope 檢查 |
一個 Hook 腳本的核心邏輯:
- 從 stdin 讀取 JSON(包含工具名稱、參數等)
- 判斷是否需要攔截
- 正常放行 →
exit 0(不輸出) - 攔截 →
exit 0+ 輸出特定 JSON(告訴 Claude「不准做」)
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "你的攔截理由,Claude 會看到這段話"
}
}
Hook 1:禁止 git add .(防止全目錄 stage)
問題:Claude 喜歡用 git add . 或 git add -A 一次 stage 所有變更,可能把 .env、credentials、或你不想 commit 的實驗檔案一起加進去。
解法:攔截所有 git add . / git add -A / git add --all,強制逐檔 stage。
#!/usr/bin/env bash
# pre-bash-git-add-guard.sh
set -uo pipefail
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
[[ "$TOOL_NAME" != "Bash" ]] && exit 0
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
[[ -z "$COMMAND" ]] && exit 0
if echo "$COMMAND" | grep -qE 'git\s+add\s+(\.|--all|-A)\b'; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"⚠️ git add . / --all / -A 禁止。請逐檔 stage:git add <specific-file>"}}'
exit 0
fi
exit 0
效果:Claude 會被強制用 git add src/main.py src/utils.py 這種方式,你在 diff 裡能清楚看到每個被 stage 的檔案。
Hook 2:Commit 前必須先看 Diff
問題:Claude 有時候改完 code 直接 git commit,你根本不知道它改了什麼就進 commit history 了。
解法:追蹤 session 內是否執行過 git diff,沒有的話攔截 git commit。
#!/bin/bash
# pre-bash-commit-diff-guard.sh
set -uo pipefail
STATE_DIR="$HOME/.claude/hooks-state"
DIFF_LOG="$STATE_DIR/git-diff-seen.log"
mkdir -p "$STATE_DIR" 2>/dev/null || exit 0
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name // ""' 2>/dev/null) || exit 0
[[ "$tool_name" != "Bash" ]] && exit 0
command=$(echo "$input" | jq -r '.tool_input.command // ""' 2>/dev/null) || exit 0
[[ -z "$command" ]] && exit 0
# 記錄 diff 執行
if printf '%s' "$command" | grep -Eq 'git\s+diff'; then
date +%s > "$DIFF_LOG" 2>/dev/null || true
exit 0
fi
# 攔截 commit(如果沒先看 diff)
if printf '%s' "$command" | grep -Eq 'git\s+commit'; then
if [[ ! -f "$DIFF_LOG" ]]; then
reason="📋 git commit 前必須先執行 git diff 確認變更內容。請先跑 git diff --stat 再 commit。"
jq -cn --arg reason "$reason" \
'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":$reason}}'
exit 0
fi
# diff 看過了,清除狀態(下次 commit 要重新看)
rm -f "$DIFF_LOG" 2>/dev/null
fi
exit 0
效果:Claude 會被迫先跑 git diff --stat,你在對話裡能看到變更摘要,確認沒問題再 commit。
Hook 3:危險命令硬攔截
問題:git push --force、rm -rf、DROP TABLE — 這些命令一旦執行就很難回滾。
解法:偵測危險關鍵字,直接 deny。
#!/bin/bash
# pre-bash-danger-guard.sh
set -uo pipefail
input=$(cat)
# Fast path: 不含危險關鍵字直接放行
case "$input" in
*'--force'*|*'reset --hard'*|*'clean -f'*|*'rm -rf'*|\
*'mkfs'*|*'shutdown'*|*'reboot'*|\
*'DROP '*|*'drop '*|*'DELETE FROM'*|*'delete from'*)
;; # 可能危險,繼續檢查
*)
exit 0
;;
esac
tool_name=$(echo "$input" | jq -r '.tool_name // ""')
[[ "$tool_name" != "Bash" ]] && exit 0
command=$(echo "$input" | jq -r '.tool_input.command // ""')
[[ -z "$command" ]] && exit 0
# Git 危險操作
if echo "$command" | grep -qE 'git\s+push\s+.*--force|git\s+push\s+-f\b'; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"🛑 git push --force 禁止。請使用 --force-with-lease 或確認後手動執行。"}}'
exit 0
fi
if echo "$command" | grep -qE 'git\s+reset\s+--hard'; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"🛑 git reset --hard 禁止。請先 git stash 保存工作進度。"}}'
exit 0
fi
# 系統毀滅指令
if echo "$command" | grep -qE 'rm\s+-rf\s+(/|~|\$HOME)'; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"🛑 危險的 rm -rf 指令。目標路徑可能影響系統或家目錄。"}}'
exit 0
fi
# DB 危險操作
if echo "$command" | grep -qiE 'DROP\s+(DATABASE|TABLE)|DELETE\s+FROM\s+\S+\s*;'; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"🛑 DB 危險操作。DROP/DELETE 需要手動確認後執行。"}}'
exit 0
fi
exit 0
要點:開頭的 case 語句是 fast path — 大多數正常指令不含這些關鍵字,直接 exit 0 跳過,幾乎零開銷。
Hook 4:敏感檔案偵測(.env / credentials)
問題:Claude 可能讀取或輸出含有 API key 的檔案內容,這些 key 會出現在你的對話 context 裡。
解法:偵測涉及敏感檔案的命令,發出警告讓 Claude 自動遮蔽。
#!/bin/bash
# pre-bash-secrets-guard.sh(警告版,不阻擋)
set -uo pipefail
input=$(cat)
# Fast path
case "$input" in
*'.env'*|*'API_KEY'*|*'SECRET'*|*'TOKEN'*|*'PASSWORD'*|\
*'PRIVATE_KEY'*|*'credentials'*|*'key.json'*|*'.pem'*)
;;
*)
exit 0
;;
esac
tool_name=$(echo "$input" | jq -r '.tool_name // ""')
[[ "$tool_name" != "Bash" ]] && exit 0
command=$(echo "$input" | jq -r '.tool_input.command // ""')
[[ -z "$command" ]] && exit 0
# 檢查是否涉及敏感檔案
if echo "$command" | grep -qE '\.(env|pem|key|credentials|key\.json)(\s|$|")'; then
echo "⚠️ SECRETS GUARD: 這個命令可能涉及敏感檔案。請在輸出中遮蔽所有 API keys、tokens、passwords。"
fi
exit 0
設計選擇:這個 hook 只警告,不阻擋。因為有時你確實需要 Claude 幫你處理 .env,但它會在輸出中自動用 *** 取代敏感值。如果要硬擋,把 echo 那行改成 deny JSON 即可。
Hook 5:Python 檔案自動 Lint
問題:Claude 改完 Python 檔案後,你得手動跑 linter 檢查格式和錯誤。
解法:用 FileChanged 事件,每次 .py 檔案被修改就自動跑 ruff check。
#!/usr/bin/env bash
# file-changed-lint.sh — FileChanged hook
set -euo pipefail
input=$(cat)
file_path=$(echo "$input" | jq -r '.file_path // ""' 2>/dev/null)
[[ -z "$file_path" ]] && exit 0
[[ "$file_path" != *.py ]] && exit 0
if ! command -v ruff &>/dev/null; then
exit 0
fi
issues=$(ruff check "$file_path" --no-fix 2>/dev/null | head -5)
if [[ -n "$issues" ]]; then
printf '{"message": "ruff: %s"}' "$(echo "$issues" | tr '\n' ' ' | head -c 200)"
fi
效果:Claude 會立刻看到 lint 錯誤,通常它會自動修復。比你事後在 CI 裡才發現省太多時間了。
擴展:同樣的模式可以套用到任何語言 — TypeScript 用 eslint、Go 用 go vet、Rust 用 clippy。只要改 matcher 和命令。
Hook 6:撞牆停手(同一命令重試 3 次就擋)
問題:Claude 遇到錯誤時會不斷重試同一個命令,有時候重試 10 次都是同樣的結果,白燒 token。
解法:追蹤每個命令的執行次數,5 分鐘內同一命令執行 2 次後,阻擋第 3 次。
#!/bin/bash
# pre-bash-retry-guard.sh
set -uo pipefail
STATE_DIR="$HOME/.claude/hooks-state"
STATE_FILE="$STATE_DIR/bash-retry.log"
mkdir -p "$STATE_DIR" 2>/dev/null || exit 0
touch "$STATE_FILE" 2>/dev/null || exit 0
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name // ""' 2>/dev/null) || exit 0
[[ "$tool_name" != "Bash" ]] && exit 0
command=$(echo "$input" | jq -r '.tool_input.command // ""' 2>/dev/null) || exit 0
[[ -z "$command" ]] && exit 0
# 白名單:唯讀命令不限制重試
case "$command" in
"git status"*|"git log"*|"git diff"*|"ls"*|"cat"*|\
"head"*|"tail"*|"grep"*|"which"*|"pwd"*)
exit 0
;;
esac
# 計算命令 hash
cmd_hash=$(printf '%s' "$command" | md5sum | cut -c1-16)
now=$(date +%s)
cutoff=$((now - 300)) # 5 分鐘窗口
# 清理過期記錄 + 計算重試次數
count=0
temp_file=$(mktemp)
while IFS='|' read -r ts hash; do
[[ -z "$ts" ]] && continue
if (( ts > cutoff )); then
echo "$ts|$hash" >> "$temp_file"
[[ "$hash" == "$cmd_hash" ]] && ((count++))
fi
done < "$STATE_FILE"
# 記錄本次
echo "$now|$cmd_hash" >> "$temp_file"
mv "$temp_file" "$STATE_FILE"
if (( count >= 2 )); then
reason="🔄 RETRY GUARD: 這個命令 5 分鐘內已執行 $count 次且未成功。請停下來分析 root cause,不要繼續重試。"
jq -cn --arg reason "$reason" \
'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":$reason}}'
exit 0
fi
exit 0
這個 hook 是省錢神器。 我實測過,一個卡住的 build 錯誤如果沒有這個 hook,Claude 會重試 5-8 次(每次都帶完整 context),大概燒掉 10-20 萬 token。有了它,第 3 次就會停下來告訴你「我卡住了,需要你看一下」。
Hook 7:治理檔案保護
問題:Claude 可能會「好心地」幫你改 CLAUDE.md、settings.json 或其他治理檔案,結果把你的規則覆蓋掉了。
解法:對特定路徑下的檔案,攔截所有 Edit/Write 操作。
#!/bin/bash
# pre-edit-governance-guard.sh
set -uo pipefail
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name // ""')
case "$tool_name" in
Edit|Write) ;;
*) exit 0 ;;
esac
file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')
[[ -z "$file_path" ]] && exit 0
# 保護的路徑清單
case "$file_path" in
*/.claude/CLAUDE.md|*/.claude/settings.json|*/.claude/settings.local.json)
reason="🛡️ GOVERNANCE GUARD: 核心治理檔案受保護。請說明要改什麼,由使用者手動修改。"
;;
*/.claude/rules/*)
reason="🛡️ GOVERNANCE GUARD: rules/ 是治理規則目錄。新增或修改請先寫到 ~/Documents/ 讓使用者 review。"
;;
*)
exit 0
;;
esac
jq -cn --arg reason "$reason" \
'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":$reason}}'
exit 0
為什麼需要這個:CLAUDE.md 和 rules/ 是你對 AI 的「法律」。如果 AI 能自己改自己的法律,那法律就沒有意義了。這個 hook 確保治理檔案的修改權永遠在人類手上。
Hook 8:Token 節省改寫(RTK)
問題:Claude 每次跑 git status、ls -la、find . 這些命令,回傳的結果會塞進 context,吃掉大量 token。
解法:用 RTK(Rust Token Killer)自動改寫常見命令,過濾掉 AI 不需要的輸出。
#!/usr/bin/env bash
# rtk-rewrite.sh(簡化版概念)
# 實際版本由 rtk 官方維護,這裡展示核心概念
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name // ""')
[[ "$tool_name" != "Bash" ]] && exit 0
command=$(echo "$input" | jq -r '.tool_input.command // ""')
# 用 rtk rewrite 改寫命令(省 60-90% token)
rewritten=$(echo "$input" | rtk rewrite 2>/dev/null)
if [[ $? -eq 0 && -n "$rewritten" ]]; then
echo "$rewritten"
exit 0
fi
exit 0
效果:git status 的輸出從幾百行壓縮到關鍵資訊,ls -la 過濾掉 AI 不需要的 metadata。實測每個月省下 60-90% 的 token,相當可觀。
怎麼裝?完整的 settings.json 範例
把上面的腳本存到 ~/.claude/scripts/ 目錄,然後在 ~/.claude/settings.json 加入:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/scripts/pre-bash-git-add-guard.sh",
"timeout": 3
},
{
"type": "command",
"command": "bash ~/.claude/scripts/pre-bash-commit-diff-guard.sh",
"timeout": 3
},
{
"type": "command",
"command": "bash ~/.claude/scripts/pre-bash-danger-guard.sh",
"timeout": 5
},
{
"type": "command",
"command": "bash ~/.claude/scripts/pre-bash-secrets-guard.sh",
"timeout": 5
},
{
"type": "command",
"command": "bash ~/.claude/scripts/pre-bash-retry-guard.sh",
"timeout": 3
}
]
},
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/scripts/pre-edit-governance-guard.sh",
"timeout": 3
}
]
}
],
"FileChanged": [
{
"matcher": "*.py",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/scripts/file-changed-lint.sh",
"timeout": 5
}
]
}
]
}
}
安裝步驟
# 1. 建目錄
mkdir -p ~/.claude/scripts ~/.claude/hooks-state
# 2. 把上面的腳本存進去
# (每個 hook 存成對應的 .sh 檔案)
# 3. 加執行權限
chmod +x ~/.claude/scripts/*.sh
# 4. 編輯 settings.json
# (把上面的 JSON 合併進你的 settings.json)
# 5. 重啟 Claude Code session
設計 Hook 的 4 個原則
1. Fast Path 先行
大多數命令是安全的。用 case 或簡單的 pattern match 在開頭快速放行,只對可疑命令做進一步檢查。Hook 有 timeout(通常 3-5 秒),如果你的 hook 太慢,會直接被跳過。
2. 能 warn 就不要 deny
不是每個 hook 都需要硬攔截。Secrets guard 用警告而不是阻擋,因為有時你確實需要 Claude 處理 .env。太多 deny 會讓 Claude 不斷重試或繞路,反而更浪費 token。
3. 白名單 > 黑名單
retry-guard 用白名單排除唯讀命令(git status、ls、grep),而不是嘗試列舉所有危險命令。因為你不可能列完所有危險操作,但你可以列完所有安全操作。
4. 狀態用檔案,不用環境變數
Hook 是獨立的 shell process,沒有共享記憶體。需要跨次呼叫的狀態(像 commit-diff-guard 的「有沒有看過 diff」),用檔案儲存在 ~/.claude/hooks-state/。
結語:從「請你不要」到「你不能」
CLAUDE.md 是給 AI 的建議。Hook 是自動執行的法律。
兩者不是互斥的 — CLAUDE.md 處理模糊的判斷(「優先用更簡單的方案」),Hook 處理明確的紅線(「禁止 git add .」)。好的規則不靠 AI 自律,靠系統設計。
從今天開始,挑一個最困擾你的問題,寫一個 hook。你會發現,比在 CLAUDE.md 裡寫十遍「不要做 X」有效得多。
我是江中喬,專注於 AI Agent Architecture、Memory Governance 與 Cognitive Diversity,持續研究如何打造能夠長期協作、可信任且可治理的 AI 系統。