Claude Code の Hooks を本番導入してしばらく経つと、必ず一度は遭遇する瞬間があります。ローカルでは綺麗に動いていたスクリプトが、本番の CI ランナーや別メンバーの環境だと沈黙します。echo も出ない、ログも残らない、Claude の挙動にも影響が見えありません。原因を掴む取っ掛かりが、文字通り何もない状態です。
ここでは私が実案件で Hooks を運用する中で積み上げてきた、本番の「発火しない」を体系的に潰していく手順をまとめました。表面的な「settings.json を見直しましょう」ではなく、そもそもなぜログが取れないのか、どうすれば再現できない現象を再現できるのかという、一段下のレイヤーの話をします。
最初に疑うべきは「いつ発火したか」ではなく「どこに出力されたか」
Hooks が動かないと感じたとき、多くの人は「本当に呼ばれたのか」から調査を始めます。ですが、私の経験ではこの順序は遠回りになることが多いです。ほとんどのケースで Hooks 自体は呼ばれていて、ただ標準出力・標準エラーの行き先を取り逃しているだけ、という結論に落ち着きます。
Claude Code は Hooks のプロセスを子プロセスとして fork し、標準入力から JSON を流し込みます。標準出力は基本的に Claude 側の挙動制御に使われ、標準エラーはユーザーが目にしない場所に吸い込まれることがあります。ターミナルで実行している時は見えていた print 文が、バックグラウンドで起動された Claude Code からは見えない、というだけの話です。
ですから本番デバッグで最初に手を入れるべきは、スクリプトの内容ではなく「出力先の固定化」です。
#!/usr/bin/env bash
# hook-entry.sh — 必ず同じ場所にログを残す土台
LOG_DIR="${HOME}/.claude/hooks-debug"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/$(date +%Y%m%d).log"
{
echo "---"
echo "time: $(date -Iseconds)"
echo "pid: $$"
echo "hook: $CLAUDE_HOOK_EVENT"
echo "tool: $CLAUDE_TOOL_NAME"
echo "cwd: $(pwd)"
echo "stdin: $(cat)"
} >> "$LOG_FILE" 2>&1このラッパを噛ませてから、本来のスクリプトを呼ぶ設計にします。ポイントは「Hooks が発火してから、ユーザーの処理にたどり着くまでの間」に、必ず1行が残る場所を確保することです。この1行があるかないかで、「呼ばれていない」のか「呼ばれているが自分の処理が壊れている」のかが即座に分かれます。
私は過去、この切り分けをせずに 2 時間かけてスクリプト内部を調べ、最終的に「そもそも matcher が間違っていて呼ばれていなかった」というオチに行き着いたことがあります。発火自体の有無は、スクリプトの品質とは独立した問題として最初に白黒を付けるべきです。
ログ不足を補う 3 層の可観測性を組む
本番で Hooks を運用するなら、可観測性は 3 層に分けて仕込んでおくと後が楽になります。私はこれを Hooks の「可視化レイヤー」と呼んでいます。
Layer 1 — 発火の生存確認。上のラッパのように、Hooks が呼ばれた事実そのものをファイルに残す層です。スクリプトの内部に一切踏み込まない、呼ばれたか呼ばれていないかだけを記録します。
Layer 2 — 入出力の記録。標準入力で渡される JSON と、スクリプトが返そうとした標準出力・exit code をそのまま保存する層です。ここまであると「Claude 側には何が届き、こちらは何を返したのか」という通信ログが得られます。
Layer 3 — 内部処理のトレース。スクリプト内部のロジックを追うデバッグログです。ここは案件ごと、用途ごとに設計が分かれるので、Layer 1-2 をきちんと敷いた上で必要な粒度に決めます。
これを 1 枚の bash で雑にまとめると、こんな形になります。
#!/usr/bin/env bash
set -euo pipefail
LOG_DIR="${HOME}/.claude/hooks-debug"
mkdir -p "$LOG_DIR"
TRACE="$LOG_DIR/trace-$(date +%Y%m%d-%H%M%S)-$$.log"
# Layer 1: fire
echo "[fire] $(date -Iseconds) event=$CLAUDE_HOOK_EVENT tool=${CLAUDE_TOOL_NAME:-}" \
>> "$LOG_DIR/fire.log"
# Layer 2: I/O capture
STDIN_JSON=$(cat)
echo "$STDIN_JSON" > "$TRACE.in.json"
# 本来のロジック
RESULT=$(echo "$STDIN_JSON" | your-actual-hook 2>> "$TRACE.err")
EXIT=$?
# Layer 2: response capture
echo "$RESULT" > "$TRACE.out.json"
echo "exit=$EXIT" > "$TRACE.exit"
# Claude に返す
echo "$RESULT"
exit $EXITやり過ぎに見えるかもしれませんが、これが入っているのといないのとでは、障害発生時の調査時間が桁で変わります。本番の Hooks は「動いている時は何も起きない」システムなので、動かなくなった瞬間にいきなり情報ゼロから掘り始めることになりがちです。平時の可観測性の貯金が、そのまま障害時の復旧速度になります。
発火条件を分離テストする — 最小再現環境を60秒で作る
「ローカルでは動くのに本番では動かない」系の問題で一番しんどいのは、再現手順の構築です。本番の設定は複雑で、プロジェクト固有の依存も多く、手元で同じ状況を作ろうとすると時間がかかります。私は、Hooks の再現環境はとにかく速く作ることを重視しています。具体的には、60 秒以内に「問題の Hooks だけが載った状態」を立ち上げられるようにしておきます。
ポイントは、本番プロジェクトから設定ファイルをコピーするのではなく、Hooks 1 本だけをテストする最小プロジェクトを常に持っておくことです。
mkdir -p /tmp/hooks-repro && cd /tmp/hooks-repro
cat > .claude/settings.json <<'JSON'
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "/tmp/hooks-repro/hook.sh" }
]
}
]
}
}
JSON
cat > hook.sh <<'SH'
#!/usr/bin/env bash
echo "[$(date -Iseconds)] hook fired" >> /tmp/hooks-repro/fire.log
cat > /tmp/hooks-repro/stdin-$$.json
exit 0
SH
chmod +x hook.shこの最小構成で Claude Code を起動して、疑似的なツール実行を 1 度走らせます。ここで動けば、Hooks の仕組み自体は確実に機能している、という基準線が引けます。動かなければ、問題は設定ファイルの場所・権限・Claude Code のバージョンといったインフラ層にあると特定できます。
本番から最小構成に「引き算しながら」近づけると時間がかかりすぎるので、最小から「足し算しながら」本番に近づけていくアプローチを私はよく使います。どこで壊れるかが逆に見える、という利点があります。
JSON 入出力と exit code の挙動をプロトコル単位で把握する
Hooks が一応発火しているのに Claude の挙動に反映されない、というもう一つの典型パターンがあります。これはほとんどの場合、exit code と標準出力の使い分けを取り違えています。
Claude Code の Hooks プロトコルは、おおまかに次のルールで動きます。
- 標準入力には JSON(イベント情報)が流れてくる
- 標準出力に JSON を返すと、Claude 側でその内容に応じた制御が行われる
- exit code が 0 なら「Hook は通過」、非 0 なら「Hook が何らかの判断をした」とみなされる
- 特定の exit code(例えば Anthropic 公式が定義する「blocking」コード)は、ツール実行を中断させる意味を持つ
ここで頻繁に見る失敗は「人間が読むためのメッセージを標準出力にそのまま流してしまい、結果として JSON パースエラーになってしまう」ケースです。echo "処理を開始します" のような気軽な出力が、Claude 側から見ると「不明な JSON が返ってきた」扱いになり、本来返すべき制御情報が無視されます。
私が必ず守っているルールは次のとおりです。
- 人間向けログは標準エラーか、専用のログファイルにだけ書く
- 標準出力には Claude プロトコル用の JSON のみを書く
- JSON を返すときは最後に必ず改行を入れる(行区切りを期待する実装があるため)
- exit code は「通過=0」「ブロック=意図した非ゼロ」の 2 値に絞って設計する
このルールを Hooks スクリプトのテンプレート段階で固定しておくと、後から運用者が変わっても事故が起きにくくなります。
マルチユーザー環境での Hooks — 権限・パス・競合の診断
チームで Claude Code を使い始めると、個人ローカルでは起きなかった問題が噴出します。特に多いのが、次の 3 つです。
- 実行権限の欠如:Hooks スクリプトに
chmod +xが付いていありません。gitはパーミッションを一部しか保存しないので、環境によって挙動が変わります。 - パスのユーザー依存:
/home/alice/bin/hook.shのような絶対パスが settings.json に書かれています。別ユーザーの環境では意味が変わります。 - 同時実行での競合:複数のエディタセッションが同じログファイルに書き込み、末尾が壊れる。
診断順序としては、まず「実行権限」を疑います。ls -l で x が立っているか確認し、立っていなければ .gitattributes または lefthook 的な仕組みで自動付与する運用に切り替えます。次に「パス」を見ます。$HOME を使うか、プロジェクトルート相対の .claude/hooks/xxx.sh に統一するのが鉄則です。最後に「競合」は、ログファイル名に $$(PID)を含めるだけで 9 割は回避できます。
さらに CI 上で Hooks を走らせている場合、ユーザーが root 相当になっていて、$HOME が /root に解決されてしまうケースもあります。本番の Hooks スクリプトで $HOME を直接使うのは、可搬性の観点では避けたいところです。プロジェクト内に自己完結させる設計の方が、長い目で見て安全です。
Hooks 本番デバッグの「再現できない」を潰す日常的な仕込み
ここまでは障害が起きてからの話をしてきましたが、本当に効くのは平時の仕込みです。Hooks の再現困難な不具合は、平時に次の 3 つを入れておくだけで大半が潰せます。
その1 — 必ず最初に来る「sentinel hook」を置く。本来やりたい処理の前に、「呼ばれた事実だけを記録する」超軽量なフックを必ず挟みます。これがあれば「呼ばれていない」のか「呼ばれたが失敗している」のかが一瞬で分かります。
その2 — バージョン情報をログに残す。Claude Code のバージョン、スクリプトの git SHA、OS・シェルのバージョンをログに残しておきます。本番でしか起きない不具合の原因は、8 割がた「どこかがバージョン違い」に帰着します。
その3 — 入力 JSON のスキーマテストを書く。Claude Code のバージョンアップで、Hooks に渡される JSON の形式が少し変わることがあります。自分のスクリプトが期待しているキーが欠けていたら即座に気付けるよう、Layer 2 で保存した JSON に対してスキーマテストをスケジュール実行しておきます。
この3つを習慣化するだけで、「ある日突然動かなくなった」系の事故はほぼ起きなくなります。逆に言うと、これらを入れないまま Hooks を本番運用していると、ある日どこかで大きく止まります。
本番障害の調査記録:実例から学ぶ切り分けフロー
最後に、私が実際に遭遇した本番障害の切り分け手順を 1 件紹介します。症状は「PreToolUse で走らせている secrets scanner が、特定メンバーの環境だけで動かない」というものでした。
最初の 1 時間は「matcher が合っていないのでは」と疑って settings.json を眺めていましたが、手がかりがありません。そこで Layer 1 のフック(呼ばれた事実だけを記録する)を差し込んだところ、問題のメンバーの環境では fire ログが全く残らないことが分かりました。この時点で、スクリプト内部ではなく設定ファイル側の問題に絞れます。
次に settings.json の読み込み順を確認します。Claude Code はユーザー設定、プロジェクト設定、ローカル設定の順でマージされるため、どこかの層で同じキーが上書きされている可能性があります。問題のメンバーのプロジェクト .claude/settings.local.json を開いたところ、古い検証用の hooks 設定が残っていて、PreToolUse をまるごと上書きしていました。これが原因でした。
このケースでポイントだったのは、最初の 1 時間を Layer 1 なしで戦っていたら、恐らく丸一日かかっていただろうということです。事実として「呼ばれていない」が確定した瞬間に、調査範囲が .claude/ 配下の設定ファイルだけに絞られました。
Hooks の本番デバッグは、派手なテクニックよりも地味な可観測性の積み重ねが効きます。ログを取るコストは小さく、ログが取れていない時の調査コストは指数的に大きい。この非対称性を理解しているかどうかが、Hooks 運用の生死を分けると感じています。
本記事のスクリプトはそのままコピーして使えるので、まずは Layer 1 の fire ログだけでも仕込んでみてください。次に Hooks が動かなくなった時、自分に数時間のデバッグ時間を与えているはずです。