結論:「書き込み前に自動チェック」を挟むだけで、手戻りが7割減る
Claude Codeが勝手にファイルを書き換える不安、フック機能で「書き込み前に自動チェック」を挟むだけで劇的に解消できます。
私は1週間の実測で、手戻り回数が週23回 → 7回(約70%減) になりました。やったことはシンプルで、Claude Codeの フック機能(Hooks) を使い、ファイル書き込みの前後に品質チェックと通知を自動で差し込んだだけです。
この記事では、フック機能の仕組みから実装テンプレート、そして実測データまでを一気に共有します。
環境・前提条件
| 項目 | バージョン / 条件 |
|---|---|
| Claude Code | 最新安定版(2025年6月時点) |
| Node.js | v20 LTS |
| TypeScript | 5.x |
| ESLint | 9.x(Flat Config) |
| OS | macOS Sonoma / Ubuntu 24.04 で動作確認 |
| Slack通知 | Incoming Webhooks 使用 |
1. フック機能(PreToolUse / PostToolUse)とは何か
公式ドキュメントの要点
Claude Codeの Hooks は、AIエージェントのツール呼び出しの 前後にユーザー定義のシェルコマンドを実行 できる仕組みです。設定ファイル(.claude/settings.json など)に宣言的に記述します。
公式ドキュメントでは、以下の4つのフックポイントが定義されています。
| フックポイント | 発火タイミング | 主な用途 |
|---|---|---|
| PreToolUse | ツール実行の 直前 | 入力のバリデーション、実行のブロック |
| PostToolUse | ツール実行の 直後 | 結果の検証、外部通知 |
| Notification | Claude Codeが通知を送るとき | カスタム通知連携 |
| Stop | エージェントがターン終了時 | 後処理、サマリー生成 |
フックのシェルコマンドには stdin経由でJSON が渡され、stdout に返すJSONの内容で 処理の続行・ブロック・フィードバック を制御できます。
重要なのは、PreToolUseフックが "decision": "block" を返すと、ツール自体が実行されない点です。これにより「壊れたコードがファイルに書き込まれる前に止める」という品質ゲートが実現できます。
2. 実装:Write系ツール発火前にESLint + 型チェックを自動実行するPreToolUseフック
考え方
Claude Codeの Write / Edit ツールが発火する前に、書き込み予定の内容を一時ファイルに書き出し、ESLintとtsc --noEmitを走らせます。エラーがあればブロックし、Claude Codeにフィードバックします。
フック設定(.claude/settings.json)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"command": "bash .claude/hooks/pre-write-check.sh"
}
]
}
}
matcher にはツール名の正規表現を指定します。Write と Edit の両方にマッチさせています。
フックスクリプト(.claude/hooks/pre-write-check.sh)
#!/usr/bin/env bash
set -euo pipefail
# stdinからJSONを読み取る
INPUT=$(cat)
# ツール入力からファイルパスと内容を取得
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty')
# 対象外ファイルはスキップ(.ts / .tsx / .js / .jsx のみチェック)
if [[ ! "$FILE_PATH" =~ \.(ts|tsx|js|jsx)$ ]]; then
echo '{}'
exit 0
fi
# 一時ファイルに書き出してチェック
TEMP_DIR=$(mktemp -d)
TEMP_FILE="$TEMP_DIR/$(basename "$FILE_PATH")"
echo "$CONTENT" > "$TEMP_FILE"
ERRORS=""
# ESLintチェック(軽量ルールのみ適用)
ESLINT_RESULT=$(npx eslint --no-eslintrc --rule '{"no-syntax-error": "error"}' \
--config .claude/hooks/eslint.quick.config.mjs \
"$TEMP_FILE" 2>&1) || {
ERRORS="ESLint errors:\n$ESLINT_RESULT"
}
# 型チェック(対象ファイルのみ高速チェック)
if [[ "$FILE_PATH" =~ \.tsx?$ ]]; then
TSC_RESULT=$(npx tsc --noEmit --pretty false \
--strict --skipLibCheck \
"$TEMP_FILE" 2>&1) || {
ERRORS="$ERRORS\nTypeScript errors:\n$TSC_RESULT"
}
fi
# クリーンアップ
rm -rf "$TEMP_DIR"
# エラーがあればブロック
if [[ -n "$ERRORS" ]]; then
jq -n --arg reason "$ERRORS" '{
"decision": "block",
"reason": $reason
}'
else
echo '{}'
fi
ポイント
- 一時ファイル方式により、既存のプロジェクトファイルを一切汚さない
- ESLintは 最小限のルールセット(構文エラー+致命的パターンのみ)に絞る(理由は後述)
-
jqでJSONを組み立てて stdout に返すだけというシンプルさ
3. 実装:PostToolUseでdiff要約をSlackに自動通知する仕組み
ファイルが書き換わった後に、変更内容の要約をSlackに飛ばします。チームで使う場合、「AIが何を変えたか」の透明性が信頼構築に直結します。
フック設定の追加
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"command": "bash .claude/hooks/pre-write-check.sh"
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "bash .claude/hooks/post-notify-slack.sh"
}
]
}
}
通知スクリプト(.claude/hooks/post-notify-slack.sh)
#!/usr/bin/env bash
set -uo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // "unknown"')
# git diff(ステージング前の差分を取得)
DIFF=$(git diff -- "$FILE_PATH" 2>/dev/null | head -50)
if [[ -z "$DIFF" ]]; then
exit 0 # 差分なしなら通知不要
fi
# 変更行数を集計
ADDED=$(echo "$DIFF" | grep -c '^+[^+]' || true)
DELETED=$(echo "$DIFF" | grep -c '^-[^-]' || true)
# Slack通知(バックグラウンドで実行してブロックしない)
SLACK_WEBHOOK_URL="${CLAUDE_SLACK_WEBHOOK:-}"
if [[ -n "$SLACK_WEBHOOK_URL" ]]; then
PAYLOAD=$(jq -n \
--arg file "$FILE_PATH" \
--arg added "+$ADDED" \
--arg deleted "-$DELETED" \
'{
"text": ("🤖 Claude Code がファイルを変更しました\n📄 `" + $file + "`\n📊 " + $added + " 行追加 / " + $deleted + " 行削除")
}')
curl -s -X POST -H 'Content-Type: application/json' \
-d "$PAYLOAD" "$SLACK_WEBHOOK_URL" &
fi
# PostToolUseはブロック判定不要なので空JSONを返す
echo '{}'
ポイント
-
curlをバックグラウンド実行(&)して、通知のレイテンシがClaude Codeの体験に影響しないようにする - Webhook URLは環境変数で管理し、設定ファイルにシークレットを書かない
-
head -50で差分の先頭50行に制限(巨大diffがSlackを埋め尽くすのを防止)
4. 実測データ:フック導入前後での手戻り回数・所要時間の比較
私のチーム(3名)で、1週間ずつ フックなし / フックあり の期間を設けて計測しました。
計測条件
- 対象プロジェクト:TypeScript + React のWebアプリ(約200ファイル)
- 「手戻り」の定義:Claude Codeの出力に対して「これ間違ってるからやり直して」と再指示した回数
- 各メンバーが日報で手戻り回数と所要時間を記録
結果
| 指標 | フックなし(Week 1) | フックあり(Week 2) | 改善率 |
|---|---|---|---|
| 手戻り回数(週合計) | 23回 | 7回 | ▲ 69.6% |
| 手戻り1回あたりの平均修正時間 | 8.2分 | 5.1分 | ▲ 37.8% |
| 週あたりの手戻り総時間 | 188.6分 | 35.7分 | ▲ 81.1% |
| フックによるブロック回数 | — | 31回 | — |
考察
- フックがブロックした31回のうち、大半がTypeScriptの型エラー(22回)でした。Claude Codeは文脈を理解して正しいロジックを書くものの、型定義の不一致を見落とすケースが多い傾向です
- 残りのESLintブロック(9回)は、未使用importやセミコロン漏れなど軽微なものが中心
- フックありでも発生した7回の手戻りは、ロジックの要件認識ミス(型やlintでは検出できない)が原因でした
5. 落とし穴:フックが重すぎると体験が崩壊する
実行時間の閾値設計が最重要
フックの最大の落とし穴は、チェックが遅いとClaude Codeの対話テンポが致命的に崩れることです。
私の実験では、以下の感覚的な閾値が見えました。
| フック実行時間 | 体感 |
|---|---|
| 〜500ms | ほぼ気にならない ✅ |
| 500ms〜2s | 「少し待たされるな」と感じる ⚠️ |
| 2s〜 | ストレスで使い続けられない ❌ |
高速化のために実践したこと
- ESLintルールを6個に厳選(全ルール適用だと3秒超 → 厳選後200ms)
-
tsc --skipLibCheckでnode_modules内の型チェックをスキップ - 対象ファイル拡張子でのフィルタリング(markdownやJSONは即スキップ)
- PostToolUseの外部通信はバックグラウンド実行(待たない)
6. すぐ使えるフック設定ファイルのテンプレート
以下のディレクトリ構成をプロジェクトにコピーすれば、すぐに使い始められます。
.claude/
├── settings.json # フック設定
└── hooks/
├── pre-write-check.sh # PreToolUse: ESLint + 型チェック
├── post-notify-slack.sh # PostToolUse: Slack通知
└── eslint.quick.config.mjs # 軽量ESLint設定
軽量ESLint設定(eslint.quick.config.mjs)
export default [
{
files: ["**/*.{js,jsx,ts,tsx}"],
rules: {
"no-undef": "error",
"no-const-assign": "error",
"no-dupe-keys": "error",
"no-unreachable": "error",
"no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"no-duplicate-imports": "error",
},
},
];
設定の全体像(.claude/settings.json)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"command": "bash .claude/hooks/pre-write-check.sh"
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "bash .claude/hooks/post-notify-slack.sh"
}
]
}
}
Tip: プロジェクトルートの
.claude/settings.jsonに書けばプロジェクトスコープ、~/.claude/settings.jsonに書けばグローバルスコープで適用されます。
まとめ
- PreToolUseフックで「書き込み前の品質ゲート」を設置すれば、型エラーやlint違反がファイルに書き込まれる前にブロックでき、手戻りが約7割減る
- PostToolUseフックで変更通知を自動化すれば、AIの変更がチームに対して透明になり、レビュー負荷が下がる
-
フックの実行時間は500ms以内に抑えるのが鉄則。ESLintルールの厳選と
tsc --skipLibCheckが効果的