Claude Code hooks:エージェントのルールを確実な保証に変える方法
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 | セッション終了時 | 後片付け、ログの記録 |
Notification | Claude 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 に置いてください。
このテーマはコース「Harness Engineering: инженерия надёжности AI-агентов」で実践とともに詳しく解説しています。
- Агент написал «готово», а фича не работает: verification gap и Definition of Done
- Все тесты зелёные, а фича сломана: E2E-тесты и архитектурные правила для AI-агента
- Автономный AI-агент часами без присмотра: Loop Engineering и условия остановки