PreToolUse / PostToolUse の使い分け
これが最も使う頻度の高いペアです。
PreToolUse の役割 :ツール実行前に「本当にこのコマンド、実行しても大丈夫か」を判定します。
実装例:git コマンド実行前に、ステージされていない変更がないかを確認します。
{
"hook_type" : "PreToolUse" ,
"event" : "git" ,
"validation" : {
"check_unstaged_changes" : true ,
"allow_force_push" : false ,
"require_commit_message" : true
},
"action" : "block_if_unstaged"
}
このフックを設定すると、Claude Code が git push をしようとした時に、自動的に git status を先に実行してくれます。未ステージの変更があれば、Claude Code に「これらの変更はコミットされていません。続行しますか?」と確認させられます。
PostToolUse の役割 :ツール実行後に「このコマンド、本当に成功したか」を判定します。
npm install したけど、実は依存関係の競合があった。git merge したけれど、実はコンフリクトが残っている。そういう「表面上は成功だけど、実は微妙」という状況を検出します。
{
"hook_type" : "PostToolUse" ,
"event" : "npm" ,
"validation" : {
"check_for_warnings" : true ,
"verify_package_lock" : true ,
"run_audit_on_install" : true
},
"action" : "report_and_ask"
}
このフックを設定すると、npm install の後に自動的に npm audit を実行して、セキュリティ脆弱性がないかを確認します。脆弱性があれば、Claude Code に「パッチ推奨」のアドバイスをさせます。
使い分けのポイント :
「実行してほしくない状況を防ぎたい」→ PreToolUse
「実行した結果を確認したい」→ PostToolUse
多くの人は、この判定で迷って「両方設定しちゃえ」となり、結果として「同じチェックが 2 回走る」という無駄が生じます。
UserPromptSubmit でできること(実例)
ユーザーがメッセージを送信した直後、Claude Code が処理を開始する「直前」に何かしたい場合、このフックです。
典型的なユースケース:ユーザーが手動でコードをペーストした時に、それが本当に実行可能な形かどうかを判定したい。
{
"hook_type" : "UserPromptSubmit" ,
"validation" : {
"detect_code_paste" : true ,
"parse_and_validate" : true ,
"suggest_fixes" : true
},
"action" : {
"if_invalid_syntax" : "ask_user_to_fix" ,
"if_missing_imports" : "auto_inject_imports" ,
"if_dependencies_missing" : "suggest_npm_install"
}
}
このフックを設定すると、ユーザーがコードをペーストした時に自動的に以下が起きます:
コードの構文をパースして、SyntaxError がないか確認
import/require が足りていないか確認
依存パッケージが node_modules にあるか確認
実際の例:ユーザーが stackoverflow のコード例をペーストしました。でも、そのコードは lodash の _.groupBy() を使ってます。でも package.json に lodash がありません。
通常なら、Claude Code はこのコードを実行しようとして「Error: Cannot find module 'lodash'」で失敗します。でも UserPromptSubmit フックがあれば「このコードは lodash が必要なので、npm install lodash を実行してもいいですか?」とユーザーに聞いてくれます。
もう1つ、セキュリティの観点から重要な使い方:ユーザー入力に SQL インジェクションの可能性がないか自動検出します。
{
"hook_type" : "UserPromptSubmit" ,
"security" : {
"detect_sql_injection_patterns" : true ,
"detect_command_injection" : true ,
"sanitize_shell_inputs" : true
},
"action" : {
"if_injection_risk_high" : "block_and_warn" ,
"if_injection_risk_medium" : "ask_confirmation"
}
}
Stop / SubagentStop の違いと適切な配置
これが混同されることが多いです。
Stop フック :セッション「全体」を終わらせるかどうかを判定します。
「この目標、達成した」と判定した時点で、後続の処理をやめたい。そういう場合に使います。
実装例:ユニットテストが全て通ったら、セッションを自動終了したい。
{
"hook_type" : "Stop" ,
"condition" : {
"all_tests_passed" : true ,
"coverage_threshold_met" : true ,
"no_warnings_or_errors" : true
},
"action" : "auto_stop"
}
このフックを設定すると、テストが全て緑になった段階で Claude Code は「目標達成」と判定して、セッションを終了します。
SubagentStop フック :「サブエージェント」の終了判定です。サブエージェントとは、大きなタスクを小分けにして、別々に処理する仕組みです。
実装例:大型プロジェクトで「フロントエンド開発」と「バックエンド開発」を並列に進める場合、バックエンド開発サブエージェントが終わったら、その結果をフロントエンド開発に反映したい。
{
"hook_type" : "SubagentStop" ,
"subagent_id" : "backend-development" ,
"condition" : {
"api_endpoints_complete" : true ,
"tests_passing" : true
},
"action" : {
"notify_parent_agent" : true ,
"pass_results_to" : "frontend-development" ,
"trigger_integration_tests" : true
}
}
Stop と SubagentStop の一番大きな違いは「スコープ」です。Stop はセッション全体、SubagentStop はサブエージェント内部。
多くの初心者は「セッション全体を止めたいのに SubagentStop を設定してしまう」という誤りをします。結果として、セッションは走り続け、予期しない追加の処理が発動します。
Session 系フックの活用
SessionStart :セッションが始まった直後に自動で実行される初期化フックです。
実装例:新しいプロジェクトを始める度に、自動的に .env.local を生成し、開発環境変数を埋め込みたい。
{
"hook_type" : "SessionStart" ,
"initialization" : {
"check_node_version" : "18.0.0" ,
"install_dependencies" : true ,
"create_env_files" : {
".env.local" : {
"NODE_ENV" : "development" ,
"DEBUG" : "true"
}
},
"run_initial_checks" : [ "git-status" , "npm-audit" ]
}
}
このフックを設定すると、セッション開始時に以下が自動で実行されます:
Node.js バージョンが 18 以上か確認
npm install を実行
.env.local ファイルを生成し、開発環境変数を埋め込む
git status と npm audit を実行
SessionEnd :セッションが終了する直前に自動で実行されるクリーンアップフックです。
実装例:セッション終了時に、生成されたコードのサマリーレポートを自動生成し、メールで送信したい。
{
"hook_type" : "SessionEnd" ,
"cleanup" : {
"generate_final_report" : true ,
"include_in_report" : [ "files_modified" , "tests_run" , "time_spent" ],
"send_report_to_email" : "developer@example.com" ,
"clean_temp_files" : true ,
"archive_session_logs" : true
}
}
Notification フックの正しい使い方
Notification は「外部システムに通知を送る」フックです。Slack メッセージ、メール、Webhook など、Claude Code の外側のシステムに情報を送ります。
実装例:git コミットが完了した時に、チャンネル #deployments に Slack メッセージを自動送信したい。
{
"hook_type" : "Notification" ,
"trigger" : "on_git_commit" ,
"conditions" : {
"branch" : "main" ,
"commit_message_pattern" : "feat:|fix:"
},
"notification" : {
"type" : "slack" ,
"webhook_url" : "https://hooks.slack.com/services/YOUR/WEBHOOK/URL" ,
"message" : {
"channel" : "#deployments" ,
"text" : "Commit to main: {commit_message}" ,
"attachments" : [
{
"fields" : [
{ "title" : "Author" , "value" : "{git_author}" , "short" : true },
{ "title" : "Timestamp" , "value" : "{timestamp}" , "short" : true }
]
}
]
}
}
}
Notification フックの使う際の注意点:
外部 API との認証 。Slack Webhook URL を JSON に直接埋め込むのは避けてください。環境変数経由で管理してください。
{
"webhook_url" : "${SLACK_WEBHOOK_URL}" ,
"auth_header" : "${SLACK_AUTH_TOKEN}"
}
私が実際にハマった3つの落とし穴
落とし穴 1:フックの発火順序の誤解
最初、私は「全てのフックは同時に発火する」と思っていました。でも実は、順序があります:
SessionStart
UserPromptSubmit
PreToolUse
PostToolUse
Notification(並列で発火)
Stop(判定)
SessionEnd
この順序を無視すると、例えば「PreToolUse で環境変数をセットしたのに、PostToolUse ではその環境変数がない」という現象が起きます。
実際のバグ例:git コマンド実行前に、GPG_KEY という環境変数をセットしたい。PreToolUse で export GPG_KEY=... を実行しました。でも、PostToolUse でそのキーを参照しようとすると「undefined」が返ってきます。
理由:PreToolUse は「シェルスクリプトの中」で実行されるので、そのスクリプトのプロセス内でのみ環境変数が有効です。PostToolUse は「別のプロセス」で実行されるため、前のスクリプトで設定した環境変数は引き継がれません。
解決策:環境変数を .env ファイルに保存して、PostToolUse でそのファイルを読み込む。
{
"hook_type" : "PreToolUse" ,
"action" : {
"command" : "echo 'GPG_KEY=${GPG_KEY}' > .env.gpg"
}
}
{
"hook_type" : "PostToolUse" ,
"action" : {
"command" : "source .env.gpg && gpg --verify signature.asc"
}
}
落とし穴 2:非同期処理の待機タイムアウト
フック内で Promise を使って非同期処理(例:外部 API 呼び出し)をした場合、Claude Code は「あ、これ非同期だ」と認識して待ってくれます。でも、その待ち時間が長いと、セッションごと失敗します。
実際のバグ例:git push 後に、CI ビルドの完了を待ちたい。GitHub REST API を使って、ビルドステータスをポーリングする非同期フックを書きました。
{
"hook_type" : "PostToolUse" ,
"event" : "git" ,
"action" : {
"wait_for_ci_build" : {
"api_endpoint" : "https://api.github.com/repos/{owner}/{repo}/actions/runs/{run_id}" ,
"poll_interval_seconds" : 5 ,
"max_wait_seconds" : 600
}
}
}
最初のテストでは 30 秒で CI ビルドが完了したので問題なかったです。でも、実際のプロジェクトで走らせると、CI ビルドに 20 分かかるプロジェクトがあり、Claude Code のセッションがタイムアウトしました。
解決策:ポーリングの最大待ち時間を明示的に設定し、タイムアウト後は「完了を待つのはやめて、非同期で進める」という設計にします。
{
"hook_type" : "PostToolUse" ,
"action" : {
"wait_for_ci_build" : {
"max_wait_seconds" : 120 ,
"on_timeout" : "continue_async_and_notify"
}
}
}
なお command timed out そのものの調整(timeout 値と分割実行)については、Hook が command timed out で失敗するときの timeout 設定と分割実行のコツ で個別に掘り下げています。長時間処理を挟むなら、あわせて目を通していただければと思います。
落とし穴 3:フック間の依存関係の循環
複数のフックを組み合わせる場合、フック A の出力がフック B の入力になる、という依存関係が発生します。でも、その依存関係が「循環」してしまうと、永遠ループになります。
実際のバグ例:
UserPromptSubmit:ユーザー入力に基づいて、自動的にコマンドを生成する
PreToolUse:生成されたコマンドをバリデートして、不安全なら修正する
PostToolUse:実行結果に基づいて、次のコマンドを生成する
この 3 つのフックを組み合わせると:
ユーザーが「git push」と入力
UserPromptSubmit が「git push -f」に変換
PreToolUse が「-f フラグは危険」と判定して「git push」に戻す
PostToolUse が「push に失敗した」と判定して「git push -f」を再試行
PreToolUse が「-f フラグは危険」と判定して「git push」に戻す
無限ループ...
解決策:各フックが「他のフックから触られてはいけないデータ」と「他のフックとデータを共有するキー」を明確に定義します。
{
"hook_type" : "UserPromptSubmit" ,
"output_key" : "user_input_raw" ,
"protected_keys" : [ "validation_state" , "retry_count" ]
}
{
"hook_type" : "PreToolUse" ,
"input_key" : "user_input_raw" ,
"output_key" : "command_validated" ,
"check_protected_keys" : true
}
各フックが「このキーはもう修正しない」という宣言をすることで、循環を防ぎます。
PostToolUse が自分自身の出力に反応して再発火してしまう典型パターンの止め方は、Claude Code Hooks が無限ループする問題 — PostToolUse が自己発火するときの止め方 にも実例をまとめています。
そのまま使えるテンプレート集
テンプレート 1:セキュリティゲート付き git push
{
"hooks" : [
{
"hook_type" : "PreToolUse" ,
"event" : "git" ,
"condition" : "command == 'push'" ,
"validations" : {
"check_branch_protection" : {
"allowed_branches" : [ "main" , "develop" ],
"require_review_for" : [ "main" ],
"block_force_push" : true
},
"check_commit_message" : {
"pattern" : "^(feat|fix|refactor|docs): \\ s" ,
"require_jira_ticket" : false
}
}
},
{
"hook_type" : "PostToolUse" ,
"event" : "git" ,
"condition" : "command == 'push'" ,
"notifications" : {
"slack" : {
"channel" : "#deployments" ,
"message" : "Pushed to {branch}"
}
}
}
]
}
テンプレート 2:自動テスト実行と結果レポート
{
"hooks" : [
{
"hook_type" : "PreToolUse" ,
"event" : "npm" ,
"condition" : "command.includes('install')" ,
"action" : {
"pre_install_check" : true ,
"verify_package_lock" : true ,
"warn_on_major_upgrades" : true
}
},
{
"hook_type" : "PostToolUse" ,
"event" : "npm" ,
"condition" : "command.includes('test')" ,
"action" : {
"verify_test_results" : true ,
"collect_coverage" : true ,
"fail_if_coverage_below" : 80 ,
"generate_report" : {
"format" : "json" ,
"output_path" : ".test-results.json"
}
}
}
]
}
テンプレート 3:セッション全体の初期化とクリーンアップ
{
"hooks" : [
{
"hook_type" : "SessionStart" ,
"initialization" : {
"check_environment" : {
"required_tools" : [ "node" , "git" , "npm" ],
"required_versions" : {
"node" : "18.0.0" ,
"npm" : "9.0.0"
}
},
"setup_project" : {
"install_dependencies" : true ,
"create_env_files" : {
".env.local" : {
"NODE_ENV" : "development" ,
"LOG_LEVEL" : "debug"
}
},
"run_initial_validation" : [ "npm audit" , "git status" ]
}
}
},
{
"hook_type" : "SessionEnd" ,
"cleanup" : {
"generate_summary" : {
"include" : [ "files_created" , "files_modified" , "tests_run" , "errors" ],
"format" : "json"
},
"send_report_to_email" : "team@example.com" ,
"archive_logs" : true ,
"cleanup_temp_files" : true
}
}
]
}
実際の設定ファイルはどう書くのか
ここまでの JSON は、責務を説明するための概念図でした。ここからは、私の環境(Claude Code v2 系)で実際に動いている設定の形をお見せします。公式ドキュメントも版によってキー名が変わるため、細部は必ずお手元のバージョンで確認していただきたいのですが、骨格は次の通りです。
フックは .claude/settings.json(プロジェクト単位)または ~/.claude/settings.json(ユーザー単位)の hooks キーに、イベント名ごとの配列として書きます。ポイントは、フックの実体が「JSON の宣言」ではなく「シェルコマンド」だということ。判定ロジックは自分のスクリプトの中に書きます。
{
"hooks" : {
"PreToolUse" : [
{
"matcher" : "Bash" ,
"hooks" : [
{
"type" : "command" ,
"command" : "$CLAUDE_PROJECT_DIR/.claude/hooks/guard-git-push.sh"
}
]
}
]
}
}
matcher はツール名にかかる正規表現です。Bash だけ、Edit|Write のように複数、あるいは空文字で全ツール、といった指定ができます。ハイフンを含むツール名の一致挙動はバージョンで厳密化された経緯があるので、アップデート後にフックが沈黙したらハイフン入り matcher の厳密一致化(v2.1.195) を先に疑ってください。
呼ばれたスクリプトは、標準入力から JSON を受け取ります。ツール名や引数はそこに入っています。
#!/usr/bin/env bash
# guard-git-push.sh
input = $( cat ) # stdin の JSON を丸ごと受け取る
cmd = $( echo " $input " | jq -r '.tool_input.command // ""' )
if echo " $cmd " | grep -qE 'git push .*(-f|--force)' ; then
echo "main への force push は禁止です。--force-with-lease を検討してください。" >&2
exit 2 # exit 2 = このツール実行をブロックし、理由を Claude に返す
fi
exit 0 # exit 0 = そのまま実行を許可
宣言型スキーマとの一番の違いは「ブロックの伝え方」です。exit 2 で終了すると、標準エラーに書いた文字列がそのまま Claude へフィードバックされ、ツール実行は止まります。exit 0 なら素通り。この二値さえ押さえれば、前半で「擬似スキーマ」として説明した検証・ゲート・通知は、すべて普通のシェルスクリプトとして書けます。逆に言えば、フックのデバッグは「そのスクリプトを単体で叩けるか」に尽きます。設定ファイルを疑う前に、スクリプトへ手元で JSON を流し込んで挙動を確かめるのが近道です。
フックのオーバーヘッドを実測する
見落とされがちですが、フックは一件ごとに別プロセスを起動します。つまり、ツールを呼ぶたびに小さな起動コストが積み上がります。普段は気にならなくても、PostToolUse に重い処理を挟むと、体感がはっきり変わります。
私自身が個人開発している壁紙アプリ(App Store と Google Play で配信しています)の CI 連携で、この差を実測しました。同じ git commit に対して、フックなし・軽量な検証スクリプト・npm audit を毎回走らせる構成の三通りで、一件あたりの追加時間を計測した結果が次の表です(手元のマシンでの中央値・参考値)。
構成 1コマンドあたりの追加時間(中央値) 体感
フックなし 0 ms 基準
軽量スクリプト(jq で分岐して即終了) 約 40〜80 ms ほぼ気づかない
PostToolUse で npm audit を毎回実行 約 1,800〜3,200 ms 明確に待たされる
数十ミリ秒のプロセス起動コストは、対話の中ではまず気になりません。問題になるのは、ネットワークやディスクを触る処理を「毎回」走らせたときです。上の npm audit は install の直後だけに絞れば十分で、全コマンドに掛ける必要はありませんでした。 実際、install の直後だけに限定したことで、監査フックの発火回数を約90%削減できました。
私の運用上の目安は「同期フックの追加時間は 200 ms 以内に収める」です。それを超える処理は、matcher と条件で発火頻度を絞るか、非同期に逃がして通知だけ受け取る設計へ寄せます。フックは速いほど「常に効いていても邪魔にならない」。この静かさこそが、フックを外さずに使い続けられる条件だと考えています。
運用上の注意点
フックのテスト
フックを本番環境に入れる前に、テスト環境で試してください。特に git / npm といった「実際のプロジェクト状態を変更する」ツールに関連するフックはテスト必須です。
テスト方法:
テスト用のダミープロジェクトを作成
フック設定を YAML で定義
Claude Code のセッションをスタート
意図通りに動いたか確認
ログとモニタリング
本番環境に入れたフックは、ログを記録してください。何か問題が起きた時に「あ、このフックが変な動きをしてた」と判定できるようにするためです。
{
"logging" : {
"enable_hook_logs" : true ,
"log_level" : "info" ,
"log_output" : "/var/log/claude-code-hooks.log" ,
"rotate_logs" : {
"max_size_mb" : 100 ,
"keep_days" : 30
}
}
}
バージョン管理
フック設定も git で管理してください。「あ、このフック設定いつから入ってたっけ」という質問に答えられるようにするためです。
git add .claude-code/hooks.json
git commit -m "feat: Add security gate for git push to main branch"
これで、Git の歴史をたどることで「いつ、どんなフック設定が導入されたか」が追跡できます。
フックは一度マスターすると、Claude Code の自動化レベルがぐっと上がります。最初は複雑に見えるかもしれませんが、「どのフックが、どのタイミングで、何を責務とするのか」という設計を明確にすれば、実務で強い武器になります。