「Claude Code に prettier の自動整形を仕込んだら、Edit と PostToolUse が交互に発火し続けて、ターミナルが滝のように流れて止まらなくなった」 — Hooks をある程度使い込んでいる方なら、一度は経験したことがあるのではないでしょうか。私自身、最初に PostToolUse と PreToolUse を本格的に使ったときに、まさにこの状態に陥り、Ctrl+C を連打して状況を止めたことを今でも覚えています。
Hooks の無限ループは、ドキュメントには「気をつけてください」程度しか書かれていませんが、実際には設定ファイルを少し書き換えただけで誰でも踏める落とし穴です。ループが起きる仕組みを順を追って整理しつつ、私が普段使っている止め方のパターンを順番にご紹介します。
なぜ Hooks は無限ループするのか
PostToolUse は、Claude Code が何らかのツール(Edit / Write / Bash など)を実行した直後に呼ばれるイベントです。ここで「実行されたツールの結果に応じて、別のコマンドを叩く」という設計が自然にできます。問題は、その「叩いたコマンド」自体が、Claude から見て新たなツール呼び出しとして観測されたり、ファイル状態を変えてしまったりするときに起こります。
典型的なパターンは次の3つです。
- 整形系の自動化が再発火を呼ぶ: Edit のあとに
prettier --writeを実行 → ファイルが書き換わる → Claude がその差分を見て「もう一度 Edit が必要」と判断 → さらに PostToolUse が発火、というループ。 - git 系コマンドが新しい Bash 呼び出しに化ける: PostToolUse(Write) のフックの中で
git add . && git commitを実行している場合、フック自身がシェル経由で動くため、ログ上は Bash 呼び出しに見えてしまい、これに反応する別の Bash 用 PostToolUse が二次発火する設計だと連鎖します。 - マーカーファイルの更新が Edit 扱いになる: チェックポイントやログ用の
.claude-last-run.jsonを書き出すフックを仕掛けると、その書き込みを別の監視系フックが拾ってさらに発火する、という同型の事故もよく見ます。
共通しているのは「フックの副作用が、フックの発火条件と被っている」という構造です。発火条件と副作用が重なる限り、論理的にループが止まる理由はありません。
まず止血する — 暴走しているフックを安全に切る
ループ中の Claude Code は、Ctrl+C 一発では止まらないことがあります。プロセスとしては動き続けて、画面だけが止まる、というケースもあります。私が最初にやることはいつも同じで、settings.json のフック定義を 物理的に空にする ことです。
# 一時的に hooks を空にして暴走を止める
cd ~/.claude
cp settings.json settings.json.bak
jq '.hooks = {}' settings.json > settings.json.tmp && mv settings.json.tmp settings.json~/.claude/settings.json のほか、プロジェクト直下の .claude/settings.json や .claude/settings.local.json にも hooks が書かれていることがあります。深いリポジトリだと複数のスコープが合成されるので、検索コマンドで一度に確認しておくと安心です。
grep -rln '"hooks"' ~/.claude .claude 2>/dev/null止血したら、バックアップから定義を1行ずつ戻して、どのフックが暴走の起点だったかを特定します。
ループを設計レベルで防ぐ3つのパターン
止血のあとは、同じ事故を二度と起こさないための恒久対策です。私が実プロジェクトで使っているパターンを3つ紹介します。
パターン1: 「再発火マーカー」で1回限りに制限する
最もシンプルで効果が大きいのが、フック側に「自分が原因の変更には反応しない」フラグを持たせる方法です。環境変数 CLAUDE_HOOK_DEPTH を見て、すでにフックが起動中なら早期 return します。
#!/bin/bash
# .claude/hooks/format.sh — 整形フックの再発火を防ぐテンプレート
if [ -n "${CLAUDE_HOOK_DEPTH:-}" ]; then
# フック内から呼ばれた処理は、もう一段ネストしない
exit 0
fi
export CLAUDE_HOOK_DEPTH=1
# 本来の処理
prettier --write "$1" >/dev/null 2>&1
# 期待出力: 何も標準出力に出さない(Claude が再判断する材料を与えない)
exit 0ポイントは「フック由来の変更は Claude に通知しない」ことです。フックから echo で結果を吐くと、Claude は「次のステップを考える材料」として読み込んでしまい、ループを誘発します。整形系のように 副作用が成功している限り報告不要 な処理は、stdout を空に保つだけでループ確率がぐっと下がります。
パターン2: matcher を絞ってフック自身の操作を除外する
Hooks 設定の matcher には正規表現が書けます。ここを丁寧に絞り込んでおくと、「フック内で実行した Bash」は対象外、といった選別が可能です。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": ".claude/hooks/format.sh",
"exclude_paths": [".claude-last-run.json", "node_modules/**", ".next/**"]
},
{
"matcher": "Bash",
"command": ".claude/hooks/log-bash.sh",
"exclude_commands_matching": "^(git |prettier|eslint|jq )"
}
]
}
}exclude_commands_matching のような除外条件は、暴走を防ぐと同時にログも見やすくなります。Hooks の挙動が想定と違う場合は、まず matcher の評価順を疑うのが基本です。詳細な発火条件の整理は Claude Code Hooksが発火しない・設定が反映されないときの原因と解決方法 に整理しているので、合わせて確認してみてください。
パターン3: ファイルロックで「同時に1つだけ」に制限する
最後の保険は、フックを flock で直列化することです。整形・コミット・通知のように、複数のフックが同じファイル群を触る場合、ロックを取って実行すれば多重発火しても順番待ちになるだけで、ループにはなりません。
#!/bin/bash
# .claude/hooks/safe-format.sh
LOCK_FILE="/tmp/claude-hooks.lock"
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
# 既に走っていれば諦める(=多重実行を許さない)
exit 0
fi
prettier --write "$1" >/dev/null 2>&1
# 期待挙動: ロックは exit 時に自動解放ロック方式は副作用の重複を許さない代わりに、長時間処理を握ると後続のフックが落ちます。私は整形のような数百ms単位の処理にだけ使い、長い処理にはマーカー方式(パターン1)を併用しています。
ループの兆候を早めに掴むためのログ設計
無限ループは、起きてから気づくのではなく「起きそうな兆候」のうちに気づきたい現象です。私はフック側に 連続発火回数 のカウンタを仕込み、ある閾値を超えたら自動で止まる仕組みにしています。
#!/bin/bash
# 連続発火を検知して自爆するフック
COUNT_FILE="/tmp/claude-hook-count"
NOW=$(date +%s)
LAST=$(cat "$COUNT_FILE.time" 2>/dev/null || echo 0)
COUNT=$(cat "$COUNT_FILE" 2>/dev/null || echo 0)
if [ $((NOW - LAST)) -lt 5 ]; then
COUNT=$((COUNT + 1))
else
COUNT=1
fi
echo "$COUNT" > "$COUNT_FILE"
echo "$NOW" > "$COUNT_FILE.time"
if [ "$COUNT" -ge 10 ]; then
echo "⚠️ Hook が5秒間に10回連続発火しました。多重実行の疑いがあるためスキップします。" >&2
exit 0
fi
# ここから本来の処理このようなセーフティを入れておけば、設定ミスで多重発火しても、被害が「画面が少し騒がしい」程度で済みます。本番運用での Hooks のトラブルシューティングについては、Claude Code Hooks の本番デバッグ術 でさらに踏み込んだ可観測性の話をまとめています。
全体を振り返って — 今日できる一歩
無限ループを完全になくす特効薬はありません。ただ、「フックの副作用が、フックの発火条件と被らないように設計する」という一点を意識するだけで、事故率は劇的に下がります。今日まず手を動かすなら、次の3つのうちどれか1つだけで構いません。
- 既存の PostToolUse フックを開いて、
stdoutに何かを出していないか確認しましょう。整形系なら、出力は空にしてください。 - matcher の
exclude_pathsかexclude_commands_matchingを1つ追加して、フック自身の副作用を除外しましょう。 - 5秒間で10回以上発火したら自動で止まる、安全装置スクリプトを
.claude/hooks/に1本置いておきましょう。
特に3つ目は、Hooks をこれから複雑にしていく予定があるなら、最初に入れておくべき土台だと考えています。Hooks 全体の設計を見直したくなったら、Claude Code Hooks 自動化マスターガイド の方も参考にしながら、ご自身のワークフローに合うパターンを選んでみてください。