手順

Claude Code hooks:エージェントのルールを確実な保証に変える方法

更新日: · Skillaria.Top

Claude Code のフックは、Claude Code 自身が特定のタイミング(ツール呼び出しの前後、プロンプト送信時、セッション開始時、エージェントの停止直前)に実行するシェルコマンドです。settings.json で設定し、終了コード2を返すとアクションをブロックしてエラーメッセージをエージェントに戻せます。そのため、CLAUDE.md のルールをモデルが忘れない保証に変えられます。

Claude Code のフックとは

フックとは、モデルではなく Claude Code 自身が、作業中の決まった瞬間に実行するコマンドです。エージェントが rm -rf を実行しようとするとフックが起動し、そのコマンドを通すかどうかを判断します。エージェントがファイルを編集すると、フックがフォーマッターを実行します。エージェントが作業を終えようとすると、フックがテストを走らせ、失敗しているうちは停止させません。

CLAUDE.md の指示との決定的な違いは、指示がテキストであることです。モデルはそれを一定の確率で考慮するにすぎません。一方フックは決定的なコードです。コンテキストがどれほど長くなっても、エージェントが何を「判断」しても、セッションの100分目にルールを忘れていても、必ず動きます。そのため、Claude Code のフックは、お願いを保証に変える主要な手段です。

CLAUDE.md のルールだけでは足りない理由

典型的な場面を考えてみてください。エージェントが機能を書き終え、ユニットテストは緑、チャットには自信満々に「完了しました」と表示されます。しかし、エンドツーエンドのシナリオは誰も実行しておらず、リンターも回しておらず、マイグレーションは途中で落ちていました。CLAUDE.md には「終了前にテストを実行すること」と書いてあったのに、です。モデルは意図的にごまかしているわけではありません。自分の仕事を自分で評価しているだけで、その自己評価は信頼できないのです。

ここからハーネスの原則が導かれます。機械的に確認できることは、モデルではなく仕組みが確認するべきです。「リンターを実行しろ」というルールは、リンターを実行するフックになります。「本番の設定ファイルに触るな」というルールは、そのファイルへの書き込みをブロックするフックになります。テキストの指示は残りますが、その役割は、停止された理由をエージェントに説明することになります。

イベント:フックが実行されるタイミング

各フックは、セッションのライフサイクルのイベントに紐づいています。主なイベントは次のとおりです。

イベント実行タイミング典型的な用途
PreToolUseツール(Bash、Edit、Write など)の呼び出し前危険なコマンドの禁止、ファイルの保護
PostToolUseツール呼び出しの成功後編集後のフォーマッター、リンター、軽量なテスト
UserPromptSubmitユーザーがプロンプトを送信した直後、処理の前コンテキストの追加、秘密情報を含むプロンプトの遮断
Stopエージェントが応答を終えようとしたとき検査に合格するまで停止させない
SubagentStopサブエージェントが作業を終えたときサブエージェントの結果の検証
SessionStartセッションの開始または再開時状態の読み込み:git status、計画、進捗ログ
SessionEndセッション終了時後片付け、ログの記録
NotificationClaude Code が通知を出したとき(許可を待っている場合など)音や、スマートフォンへのメッセージ
PreCompactコンテキストの圧縮前圧縮の前に重要な情報を保存

信頼性の観点で特に役立つのは PreToolUse、PostToolUse、Stop の3つです。エージェントの弱点である、危険な操作、検証されていない編集、早すぎる「完了」の3つを押さえられます。

設定の場所:settings.json

フックは Claude Code の設定ファイルに記述します。

  • ~/.claude/settings.json — 個人用。すべてのプロジェクトで有効。
  • リポジトリ内の .claude/settings.json — チーム共通。git にコミットします。
  • .claude/settings.local.json — プロジェクトごとのローカル設定。git には入りません。

構造は「イベント → matcher を持つグループのリスト → コマンドのリスト」です。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint.sh",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

matcher はツール名、または名前にマッチする正規表現です(例:Bash、Edit|Write、.*)。ツールに関係するイベントで使います。Stop や UserPromptSubmit では省略できます。CLAUDE_PROJECT_DIR はプロジェクトのルールを指すので、スクリプトは .claude/hooks/ に置き、この変数経由で呼び出すと便利です。現在のフックは Claude Code の中で /hooks と入力すると確認できます。

フックがデータを受け取り、応答する仕組み

入力:stdin の JSON

Claude Code はフックに、イベントの情報を含む JSON を渡します。セッション ID、作業ディレクトリ、イベント名、そしてツールの場合は tool_name と tool_input です。Bash では tool_input.command にコマンドが、Edit と Write では tool_input.file_path にファイルのパスが入ります。読み取りには jq が便利です。

{
  "session_id": "abc123",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "rm -rf build/" },
  "cwd": "/home/dev/project"
}

出力:終了コード

コード動作
0成功。作業は続行。UserPromptSubmit と SessionStart では、stdout がエージェントのコンテキストに追加される
2ブロックするエラー。stderr がエージェントに渡され、アクションは止まる(PreToolUse ではツール呼び出しがキャンセルされ、Stop ではエージェントが作業を続ける)
その他ブロックしないエラー。stderr がユーザーに表示され、作業は続く

終了コード2が中核の仕組みです。エージェントは stderr のテキストをフィードバックとして受け取り、修正できます。コマンドを書き直す、テストを直す、そのファイルに触らない、といった具合です。そのため stderr には「エラー」と書くのではなく、理由と、代わりに何をすべきかを書いてください。

終了コードのほかに、stdout に "decision": "block" と "reason" を含む JSON を出力する方法もあります。細かい制御に使えますが、多くのタスクは終了コード2と stderr のメッセージで十分です。

例1:編集のたびにリンターとテストを実行する

matcher が Edit|Write の PostToolUse フックは、ファイルが変更された後に動きます。ツールはすでに実行済みなので編集を取り消すことはできませんが、終了コード2によってリンターの出力がエージェントに戻り、30分後ではなくその場で修正されます。

#!/usr/bin/env bash
# .claude/hooks/lint.sh
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
  *.py)
    if ! out=$(ruff check "$file" 2>&1); then
      echo "$file でリンターがエラーを検出しました:" >&2
      echo "$out" >&2
      exit 2
    fi ;;
  *.php)
    if ! out=$(php -l "$file" 2>&1); then
      echo "$out" >&2
      exit 2
    fi ;;
esac
exit 0

ここに10分かかる全テストを入れてはいけません。フックは編集のたびに呼ばれます。編集直後は軽い検査(構文、リンター、該当モジュールのテスト)にとどめ、重い検査は Stop フックに任せてください。

例2:危険なコマンドを禁止する

matcher が Bash の PreToolUse フックは、コマンドが実行される前に内容を確認できます。終了コード2で呼び出しを取り消し、エージェントに理由を伝えます。

#!/usr/bin/env bash
# .claude/hooks/guard.sh
cmd=$(jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq 'rm -rf /|git push (-f|--force)|DROP (TABLE|DATABASE)'; then
  echo "プロジェクトの方針によりコマンドをブロックしました: $cmd。危険な操作は人が実行します。" >&2
  exit 2
fi
exit 0

ファイルの保護も同じ仕組みです。Edit|Write のフックで file_path を確認し、.env、本番の設定、生成されたコード、適用済みのマイグレーションファイルへの書き込みをブロックします。ただし、正規表現による検査は回避されうることに注意してください。エージェントは別の書き方でコマンドを書くかもしれません。フックは強力な安全策ですが、アクセス権限やサンドボックスの代わりにはなりません。

例3:テストが赤のうちは「完了」と言わせない

Stop フックは、エージェントが終わろうとしたときに動きます。テストが通らなければ、終了コード2で停止を止め、テストの出力を次の作業としてエージェントに渡します。これは、完了の定義(Definition of Done)を機械的に実装したものです。

#!/usr/bin/env bash
# .claude/hooks/done.sh
input=$(cat)
# 無限ループの防止:フックが一度すでに作業を延長している
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi
if ! out=$(npm test --silent 2>&1); then
  echo "テストが失敗したため、作業は完了していません:" >&2
  echo "$out" | tail -n 40 >&2
  exit 2
fi
exit 0

stop_hook_active は、エージェントが stop フックによって作業を続けている最中であれば true になります。この確認がないと、フックがエージェントを延々と回し続けることがあります。「エージェントが一晩中見当違いの作業をしている」状況がまさにこれです。自律ループでは、さらに試行回数の上限と、人を呼ぶ条件を加えてください。

例4:セッション開始時に状態を読み込む

SessionStart では、フックの stdout がコンテキストに入ります。現在のブランチ、未コミットの変更、計画の次のタスクを表示させると、エージェントがゼロから始めずに済みます。

#!/usr/bin/env bash
echo "ブランチ: $(git branch --show-current)"
git status --short | head -n 20
[ -f PROGRESS.md ] && tail -n 15 PROGRESS.md
exit 0

フックはルールを保証に変える

ハーネスのルールは3つのレベルに分けて考えると分かりやすくなります。第1レベルは CLAUDE.md のテキストです。作業の仕方を説明しますが、守られるかどうかは確率的です。第2レベルは実行可能な検査です。テスト、E2E、アーキテクチャのルールを、違反すると失敗するスクリプトとして書きます。第3レベルはフックで、モデルの関与なしに、適切なタイミングでこれらの検査を自動的に実行します。

実践的なルールは次のとおりです。エージェントが同じルールを2度破ったら、CLAUDE.md に大文字で警告を1つ追加するのではなく、その違反をスクリプトで検査できないかを問いかけてください。できるなら、スクリプトを作ってフックに登録します。そうすると、テキストの指示は短くなり、振る舞いは安定します。

テキストのルールフックによる保証
「規約どおりにコードを整形する」PostToolUse が変更されたファイルにフォーマッターを実行する
「force で push しない」Bash の PreToolUse がコマンドをブロックする(終了コード2)
「テストが赤のうちは完了と言わない」Stop がテストを実行し、作業の終了を止める
「.env を編集しない」Edit|Write の PreToolUse がパスを検査する
「どこで止まったか覚えておく」SessionStart が計画と git status を表示する

よくある間違い

  • ブロックに終了コード1を使う。 スクリプトが1で終了すると、エージェントは何も知らされず、アクションは通ってしまいます。ブロックするのは終了コード2だけです。
  • メッセージを stdout に出す。 終了コード2のとき、エージェントに届くのは stderr です。>&2 を使って書いてください。
  • 頻繁に呼ばれるイベントで重いフックを使う。 PostToolUse で全テストを回すと、編集のたびに1分かかります。重い処理は Stop に置いてください。
  • Stop が無限に続く。 stop_hook_active の確認や上限がないと、エージェントは延々と回ります。
  • 相対パスを使う。 フックはセッションの作業ディレクトリで実行されます。$CLAUDE_PROJECT_DIR を使ってください。
  • 検証していない他人のフックを入れる。 フックはあなたの権限で、確認なしに実行されます。他のリポジトリのスクリプトは、信頼する前に必ず読んでください。

まとめ

Claude Code のフックは、最も重要なルールをテキストからコードへ移します。PreToolUse は危険な操作を止め、PostToolUse はすべての編集を検査し、Stop は早すぎる完了報告を防ぎ、SessionStart はプロジェクトの記憶をエージェントに返します。まずは、エージェントがすでに破ったことのあるルールに対して2〜3個のフックから始めてください。終了コード2のメッセージは分かりやすく書きます。そうすればエージェントは自分でエラーを直し、あなたはプロセスではなく結果を確認すればよくなります。検証、完了判定、自律ループの実践的な扱い方は、このコースで取り上げています。

よくある質問

フックを変更したあと、Claude Code を再起動する必要がありますか?

settings.json の変更は、新しいセッションで確実に反映されます。現在のフック設定は、Claude Code 内で /hooks と入力して確認できます。

フックは bash 以外で書けますか?

はい。フックは任意のコマンドで、Python、Node.js のスクリプトやバイナリでも構いません。stdin から JSON を読み、正しい終了コードを返すことだけが必要です。

成功したフックの出力は、エージェントに見えますか?

通常は見えません。終了コードが0の場合、出力はコンテキストに入りません。例外は UserPromptSubmit と SessionStart で、その場合は stdout がエージェントのコンテキストに追加されます。

サブエージェントにもフックは効きますか?

はい。サブエージェントのツール呼び出しも同じ PreToolUse と PostToolUse のフックを通ります。サブエージェントの終了には SubagentStop という専用のイベントがあります。

フックを設定を git にコミットしても安全ですか?

はい。.claude/settings.json と .claude/hooks/ 内のスクリプトをチームで共有するのは一般的な方法です。個人用のフックは settings.local.json か ~/.claude/settings.json に置いてください。