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 # 元の終了コードではなく2を使い、Claudeが確実に対処するようにする
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'"
}
]
}
]
}
}
head -20で出力を制限——tscのエラーは長くなりがちで、大量のテキストをClaudeに渡すと判断を妨げます。
#!/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はグローバルとマージして両方実行されます。
Hooksで自動コードチェックを実現する核心は3ステップです:
PostToolUse + "Write|Edit" matcherでファイル書き込みを捕捉1つの言語から始めて、動いたら他の言語を追加する。スクリプトを.claude/hooks/にコミットしてチームで共有すれば、一度の設定で長期的に効果が続きます。