用 PostToolUse Hook 在 Claude 寫檔案後自動觸發 lint,搭配退出碼 2 讓 Claude 自動修復,形成寫→查→修閉環。
Claude Code 寫程式碼很快,但它不總是記得跑 lint。你讓它「寫完順便檢查一下」,它有時會做,有時會漏。Hooks 解決的就是這個問題——把程式碼檢查從「Claude 的責任」變成「系統的責任」。
這篇文章專注一件事:用 PostToolUse Hook 在 Claude 寫完程式碼後自動觸發檢查,讓每次檔案變更都符合你的程式碼規範。
程式碼檢查的 Hook 邏輯很簡單:
Write 或 Edit 寫了一個檔案關鍵是退出碼 2——它會讓 Claude 收到錯誤訊息並自動修復,形成一個「寫→查→修」的閉環。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash -c 'file=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .file_path 2>/dev/null); [[ \"$file\" == *.rb ]] && rubocop -A \"$file\" --no-color -q || true'"
}
]
}
]
}
}
-A 參數讓 RuboCop 自動修復可修復的問題。無法修復的問題會輸出到 stderr,退出碼非零,Claude 收到後會處理。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash -c 'file=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .file_path 2>/dev/null); [[ \"$file\" =~ \\.(js|ts|jsx|tsx)$ ]] && npx eslint --fix \"$file\" || true'"
}
]
}
]
}
}
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash -c 'file=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .file_path 2>/dev/null); [[ \"$file\" == *.py ]] && ruff check --fix \"$file\" && ruff format \"$file\" || true'"
}
]
}
]
}
}
實際專案往往混用多種語言。把檢查邏輯寫進獨立腳本,比在 JSON 裡堆命令更好維護。
建立 .claude/hooks/lint.sh:
#!/bin/bash
set -e
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // empty')
[[ -z "$file" ]] && exit 0
[[ ! -f "$file" ]] && exit 0
ext="${file##*.}"
case "$ext" in
rb)
rubocop -A "$file" --no-color -q
;;
js|ts|jsx|tsx)
npx eslint --fix "$file" --quiet
;;
py)
ruff check --fix "$file"
ruff format "$file"
;;
go)
gofmt -w "$file"
golangci-lint run "$file" 2>&1
;;
*)
exit 0
;;
esac
settings.json 引用這個腳本:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/lint.sh"
}
]
}
]
}
}
腳本可以加註解、可以單獨測試、可以提交到 git 和團隊共用。
退出碼 2 + stderr 輸出是讓 Claude 自動修復的關鍵:
#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // empty')
[[ -z "$file" || ! -f "$file" ]] && exit 0
ext="${file##*.}"
run_lint() {
local output exit_code
output=$(rubocop "$file" --no-color 2>&1)
exit_code=$?
if [[ $exit_code -ne 0 ]]; then
echo "$output" >&2
exit 2
fi
rubocop -A "$file" --no-color -q
}
[[ "$ext" == "rb" ]] && run_lint
型別檢查通常比 lint 慢,適合單獨設定:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash -c 'file=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .file_path 2>/dev/null); [[ \"$file\" == *.ts ]] && npx tsc --noEmit --skipLibCheck 2>&1 | head -20 || true'"
}
]
}
]
}
}
#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // empty')
[[ -z "$file" || ! -f "$file" ]] && exit 0
skip_patterns=(
"vendor/"
"node_modules/"
"db/schema.rb"
"db/queue_schema.rb"
".min.js"
"_test.go"
)
for pattern in "${skip_patterns[@]}"; do
[[ "$file" == *"$pattern"* ]] && exit 0
done
ext="${file##*.}"
[[ "$ext" == "rb" ]] && rubocop -A "$file" --no-color -q
設定好之後的工作流程:
Write 工具寫入rubocop -A,發現 3 處風格問題全程你不需要手動跑任何檢查指令。
全域(~/.claude/settings.json): 適合所有專案使用同一套規範。
專案級(.claude/settings.json 提交到 git): 團隊專案推薦,每個人開啟專案就自動有正確的設定。
兩者可以並存,專案級會和全域合併執行。
用 Hooks 做自動程式碼檢查的核心就三步:
PostToolUse + "Write|Edit" matcher 攔截檔案寫入從一個語言開始配,跑通了再加其他語言。把腳本提交到 .claude/hooks/,團隊共用,一次設定長期受益。