Claude Code Hooks実例カタログ — 9つのユースケース別レシピと避けたい落とし穴
Claude CodeのHooksを実運用に乗せる9つのレシピ集。PostToolUseでの自動フォーマット、SubagentStopでの通知、PreToolUseでの危険コマンドブロックなど、コピペで動く設定例を集めました。
要点
Claude CodeのHooksは、ツール呼び出しやセッションのライフサイクルに合わせて任意のシェルコマンドを差し込める機構です。手動で「保存後にprettierを回す」「危険コマンドを止める」といった作業を、Claude Codeの挙動と一体化させて自動化できます。
入門は別記事Claude Code Hooks入門 — 最初のHookを5ステップで動かすで扱ったので、本記事は「実運用に乗せやすい9つのレシピ」を一気に紹介します。各レシピには settings.json の最小例と「何が防げるか / 落とし穴」を添えます。設定例は公式リファレンスの現行仕様(Claude Code v2.1.285時点のdocs)と照合しています。
Hooksの全体像(おさらい)
Hooksは ~/.claude/settings.json または <project>/.claude/settings.json の hooks キーに登録します。event名 → 配列 → 各要素にマッチャとcommandという構造です。
| Event | 発火タイミング | 用途の例 |
|---|---|---|
PreToolUse | 発火タイミングツール呼び出し直前 | 用途の例危険コマンドのブロック、許可確認 |
PostToolUse | 発火タイミングツール呼び出し直後 | 用途の例フォーマット、テスト、正規化 |
UserPromptSubmit | 発火タイミングユーザーがプロンプト送信時 | 用途の例プロンプトの記録、簡易バリデーション |
SessionStart | 発火タイミングセッション開始時 | 用途の例コンテキスト読み込み、初期化 |
SessionEnd | 発火タイミングセッション終了時 | 用途の例サマリ書き出し、計測 |
Stop | 発火タイミングメインエージェント停止時 | 用途の例完了通知、後処理 |
SubagentStop | 発火タイミングサブエージェント停止時 | 用途の例完了通知、結果ログ |
Notification | 発火タイミング通知発火時 | 用途の例OS通知、Slack連携 |
PreCompact | 発火タイミングコンテキスト圧縮直前 | 用途の例スナップショット保存 |
各hookの command には標準入力経由でイベントのJSONが流し込まれるので、jq で必要な値を抜き出して使う形が定型です。
レシピを読む前に押さえる終了コードの規則
設定例の挙動は、ほぼ終了コードとイベントの組み合わせで決まります。exit 0は「成功」で、stdoutがClaudeのコンテキストに入るのは UserPromptSubmit と SessionStart など一部のイベントだけです。PostToolUse のstdoutはデバッグログに書かれるだけで、Claudeにも画面にも出ません。
exit 2は「ブロック」を意味します。PreToolUse ならツール呼び出しが止まり、PostToolUse ならツールはすでに実行済みなので、stderrがClaudeに渡されるだけです。
落とし穴はexit 1です。JSONを返さない限り、exit 1は非ブロッキングのエラーとして扱われ、処理はそのまま進みます。Unixの慣習で「失敗ならexit 1」と書いた検問フックは、何も止めません。
matcherの評価も見ておきます。英数字・_・-・空白・,・| だけなら完全一致(Write|Edit は2つのツール名のどちらか)で、それ以外の文字を含むと正規表現になります。省略・空文字・"*" は全件一致です。FileChanged と StopFailure だけは、完全一致になる文字が英数字・_・| に限られます。UserPromptSubmit と Stop はmatcherに対応しておらず、書いても無視されます。
9つの実例レシピ
レシピ1: 特定ディレクトリのMarkdownだけ保存後に整形
ドキュメントを置くリポジトリで、Claude Codeが docs/**/*.md を編集したら、保存直後に整形コマンドを走らせる例です。cd でリポジトリ直下へ移るのは、そこにある .markdownlint-cli2.* の設定を読ませるためです。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path // empty' | (read p; case \"$p\" in */docs/*.md) cd \"$(git rev-parse --show-toplevel)\" && npx --no-install markdownlint-cli2 --fix \"$p\" 1>&2 ;; esac)"
}
]
}
]
}
}防げるもの: Claudeが書いた文書に、空行の数や箇条書きの記号など書式の揺れが残ること。人間が毎回コマンドを打つ手間もなくなります。編集対象が決まったパスにあり、機械的に直せるルールがあるときに向いた型です。
落とし穴: 発火するのはClaudeの Write / Edit だけです。sed -i のようなBash経由の書き換えや、エディタなど外部プロセスによる変更では PostToolUse が走りません。ファイルの変更そのものを契機にしたいときは FileChanged イベントを使います。ただし FileChanged は事後の通知用で、書き込みを止める判断はできません。
matcher で対象ツールを絞らないと、BashやReadの度にjqが起動して遅くなります。stderrへ出しているのは、exit 0のstderrがデバッグログにしか残らず、画面を汚さないためです。
レシピ2: コードファイル保存後にPrettierを走らせる
定番のフォーマット自動化です。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path // empty' | (read p; case \"$p\" in *.ts|*.tsx|*.js|*.jsx|*.json) npx --no-install prettier --write \"$p\" 1>&2 ;; esac)"
}
]
}
]
}
}防げるもの: Claudeの書いたコードがチームの書式と食い違い、レビューの差分が整形だけで膨らむこと。eslint --fix でも同じ要領です。
落とし穴: npx --no-install を付けないと、未インストールのときにパッケージを取りに行こうとして待たされます。付けておけば、未導入の環境ではエラーで終わり、その場で気づけます。
もう1つは、整形が走るとファイルの内容が変わる点です。Claudeが直前に読んだ内容と実際のファイルがずれることがあります。整形後の差分が大きい設定(セミコロンの有無など)は、プロジェクトのPrettier設定と揃えておくのが前提です。
レシピ3: 危険なBashコマンドを実行前にブロック(PreToolUse)
rm -rf / や chmod 777 のような破壊的コマンドを、実行前に止めるパターンです。PreToolUse フックがexit 2で終わると、ツール呼び出しはブロックされ、stderrのメッセージがClaudeに渡されます。Claudeは理由を読んで別の手段を選び直せます。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.command // empty' | grep -qE 'rm[[:space:]]+-rf[[:space:]]+/|chmod[[:space:]]+777|:\\(\\)' && { echo 'Blocked: dangerous command' >&2; exit 2; } || true"
}
]
}
]
}
}防げるもの: 共有環境や本番に近い環境で、Claudeが破壊的コマンドを実行すること。PreToolUse フックは権限の判定より先に走ります。許可済みのコマンドや bypassPermissions モードでも、exit 2ならブロックできます。
落とし穴: 正規表現のブラックリストは、書き方を変えるだけですり抜けます。実際、上の rm[[:space:]]+-rf[[:space:]]+/ は rm -fr / に一致しません。rm -rf ./dist は通し、rm -rf /tmp/build は止めてしまうという過剰ブロックも同じ式で起きます。
もう1つの落とし穴は終了コードです。exit 1 で終えると非ブロッキング扱いで、コマンドはそのまま実行されます。タイムアウトしたフックも、PreToolUse ではツール呼び出しをブロックしません(通常の権限確認に進みます)。
フックの起動を絞るだけなら、"if": "Bash(rm *)" のように権限ルール構文で書けます。ただし if は最善努力のフィルタで、$TOOL git push のように展開後のコマンド名が読めない場合はフックが走ります。厳密な許可 / 拒否は、公式も permissions を使うよう案内しています。本気で守るなら、権限ルールとサンドボックスを主にして、フックは補助に置く構成が現実的です。
レシピ4: テスト自動実行(PostToolUse → npm test)
実装ファイルを編集した直後に関連テストを走らせ、壊れていたらClaudeに知らせます。Jest系のテストランナーを前提にした例です。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path // empty' | (read p; case \"$p\" in *.ts|*.tsx|*.js) cd \"$(git rev-parse --show-toplevel)\" && out=$(npm test --silent -- --bail --passWithNoTests --findRelatedTests \"$p\" 2>&1) || { echo \"$out\" | tail -20 >&2; exit 2; } ;; esac)"
}
]
}
]
}
}防げるもの: 壊れたまま次の編集へ進み、原因の特定が難しくなること。失敗したときだけexit 2で終わるので、通っている間はClaudeの視界に何も出ません。
落とし穴: テスト結果をstdoutに流して || true で終える書き方は、よくある誤りです。結果がClaudeに届きません。exit 0のstderrもデバッグログ止まりで、見せる経路はexit 2とstderrの組み合わせ(またはJSONの additionalContext)しかありません。
--findRelatedTests には編集したファイルのパスを渡す必要があります。パスを渡さないと、絞り込みとして働きません。
--passWithNoTests も欠かせません。Jestは対象のテストが0件だと失敗で終わるので、これが無いと、テストの無いファイルを編集するたびに失敗がClaudeに届きます。
それでもスイートが重いなら、"asyncRewake": true を付けて裏で走らせ、失敗したときだけexit 2でClaudeを呼び戻す手があります。または、テストは Stop 時に1回だけ走らせる構成にします。Stop フックがexit 2で終わると、Claudeは停止せず作業を続けます。
レシピ5: サブエージェント完了でmacOS通知(SubagentStop)
長時間の調査タスクをサブエージェントに任せて他作業をしているとき、完了したらOS通知で知らせる構成。
{
"hooks": {
"SubagentStop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"サブエージェント完了\" with title \"Claude Code\"' 2>/dev/null || true"
}
]
}
]
}
}防げるもの: 長い調査の完了に気づかず、他の作業の合間に止まったままになること。Linuxなら notify-send、Windowsなら powershell -c New-BurntToastNotification に置き換えます。
落とし穴: SubagentStop のmatcherが照合するのはエージェント種別(agent_type)で、Explore や Plan のように書けます。プロンプト提案や /btw の脇道質問といったClaude Code自身の内部エージェントでも SubagentStop は発火します。"*" のままだと、その度に通知が鳴りえます。
自分の作業が止まっているかどうかを知りたいだけなら、Notification イベントの方が目的に近い選択肢です。permission_prompt(許可待ちが約6秒続いたとき)や idle_prompt(応答完了から約60秒入力がないとき)で絞れるので、通知が頻発しません。
レシピ6: セッション開始時にプロジェクトコンテキストを注入(SessionStart)
毎回特定のドキュメントを読み込ませたい場合、SessionStart のstdoutがそのままClaudeのコンテキストに追加されます。PostToolUse と違い、このイベントではexit 0のstdoutが読まれます。
{
"hooks": {
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "cat $(git rev-parse --show-toplevel)/docs/current-sprint.md 2>/dev/null || true"
}
]
}
]
}
}防げるもの: 「今週の方針を伝え忘れて、古い前提で作業が進む」こと。スプリント目標や直近の障害メモなど、頻繁に書き換わる一時情報に向いています。変わらない情報は CLAUDE.md に書くのが筋です。
落とし穴: このフックは新規起動(startup)だけでなく、resume・clear・compact でも発火します。matcherを "*" にした上の例は、圧縮の後にも同じファイルを注入し直すので、圧縮で薄れた前提を戻す役割も果たします。逆に、注入が不要な場面があるなら startup|resume のように絞ります。
SessionStart は毎セッション走るので、遅いコマンドは体感に直結します。対話セッションではフックはバックグラウンドで走り、入力はすぐにできますが、Claudeの最初の応答はフックの完了を待ちます。大きなファイルを流し込めば、その分コンテキストも消費します。1回の注入は数百行までに絞るのが無難です。
レシピ7: ユーザープロンプトの記録(UserPromptSubmit)
ユーザーが投げたプロンプトを別ファイルに追記して、後で「あの時何を頼んだか」を辿れるようにします。
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "jq -r --arg t \"$(date +%FT%T)\" '\"\\($t) \\(.prompt[0:200] | gsub(\"\\n\";\" \"))\"' >> ~/.claude/prompt-log.txt"
}
]
}
]
}
}防げるもの: 「あのとき何と指示したか」が会話の流れに埋もれて再現できなくなること。プロンプトの改善材料や、作業の振り返りに使えます。
落とし穴: 入力JSONに timestamp フィールドはありません。フィールドは prompt のほか、session_id・cwd・permission_mode などです。.timestamp を読む式を書く例を見かけますが、時刻の代わりに null が並びます。上の例では、時刻をシェルの date で作って渡しています。
UserPromptSubmit はmatcherに対応しないので、matcher は書きません。書いても無視されます。また、既定のタイムアウトは他のイベントの600秒でなく30秒です。プロンプトごとに走り、終わるまでモデルの処理が始まらないため、重い処理は入れません。
機密を含むプロンプトが流れる場合は、ログの保管場所のアクセス権を絞り、クラウド同期の対象から外します。ただし、ログを書かなくてもプロンプト本文は会話のトランスクリプトなどローカルのファイルに残ります。ログを避けても、機密がディスクに残らないわけではありません。
レシピ8: コンテキスト圧縮直前のトランスクリプト退避(PreCompact)
長時間セッションでコンテキストが圧縮される直前に、会話の記録ファイルをコピーしておく安全網です。
{
"hooks": {
"PreCompact": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "jq -r '.transcript_path' | (read t; mkdir -p ~/.claude/snapshots && cp \"$t\" \"$HOME/.claude/snapshots/$(date +%Y%m%d-%H%M%S).jsonl\")"
}
]
}
]
}
}防げるもの: 大きなリファクタや調査の途中で圧縮が入り、圧縮前のやり取りの細部を後から確認できなくなること。
落とし穴: jq '.' > file でstdinを保存する書き方を見かけますが、これで残るのは「現状のコンテキスト」ではありません。PreCompact の入力JSONにあるのは trigger(manual か auto)、custom_instructions、共通フィールドの transcript_path などです。会話の中身は含まれません。中身を残したいなら、transcript_path が指す記録ファイルをコピーします。
トランスクリプトは非同期に書かれるため、フックが走った時点で直近のメッセージが未反映のことがあります。matcherは manual(/compact)と auto(自動圧縮)で分けられ、manual だけに絞れば手動圧縮のときしか走りません。圧縮後にやりたいことがあるなら、要約(compact_summary)を受け取れる PostCompact を使います。
exit 2を返すと圧縮そのものをブロックできます。ただし、コンテキスト上限のエラーからの回復として走った自動圧縮を止めると、そのままAPIエラーが表に出てリクエストが失敗します。バックアップ目的のフックではexit 2を使いません。スナップショットは溜まるので、古いものを消す運用も必要です。
レシピ9: 機微ファイル編集の警告(PostToolUse)
.env や認証情報ファイルへの編集を検出して、Claudeに確認を促す警告を出します(ブロックではなく注意喚起)。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path // empty' | grep -qE '\\.env$|credentials|secrets' && { echo '機微ファイルが編集されました。コミット前に内容を確認してください。' >&2; exit 2; } || true"
}
]
}
]
}
}防げるもの: 認証情報や環境変数ファイルを、意識しないまま編集してコミットまで進めてしまうこと。
落とし穴: 警告をstderrに出してexit 0で終える書き方は、よくある誤りです。この組み合わせでは、stderrはデバッグログにしか残りません。Claudeにも画面にも届かず、警告は誰にも読まれません。PostToolUse で警告を届けるにはexit 2にします。ツールはすでに実行済みなので、ファイルの編集は取り消されず、stderrがClaudeに渡るだけです。
検出の範囲も狭い点に注意が必要です。正規表現 \.env$ は .env.local や .env.production に一致しません。逆に secrets を含むパスは、ドキュメントのファイル名でも引っかかります。誤ってpushされた場合にgit履歴から消す手間は、このフックでは減りません。書き込みそのものを止めたいなら、PreToolUse で同じ判定をするか、権限ルールの deny に Edit(.env*) のような規則を置く方が確実です。
避けたいアンチパターン
アンチ1: 毎回フルビルドを走らせる
PostToolUse に重い処理(フルビルド / 全テスト)を仕込むと、Claude Codeの応答が常に遅くなります。1ファイル編集で30秒待つような構成は、対話のテンポを崩します。重い処理は async: true のhookか、別CLIでの手動起動に分離します。async: true のフックはブロックできませんが、結果は返せます。JSONの additionalContext と systemMessage が、次のターンでClaudeに届きます。失敗時にすぐ呼び戻したいなら asyncRewake: true を使います。exit 2でClaudeが起き、stderrがシステムリマインダーとして渡ります。SessionEnd は既定1.5秒で打ち切られ、延ばしても最大60秒なので向きません。
アンチ2: stdoutとstderrの行き先をイベントごとに確かめない
「stdoutはコンテキストに入る」と一律に思い込むと、意図が伝わりません。UserPromptSubmit・SessionStart など一部のイベントでは、exit 0のstdoutがClaudeのコンテキストに入ります。逆に PostToolUse のstdoutは、デバッグログにしか書かれません。
Claudeに伝えたいことを PostToolUse から届ける経路は、exit 2のstderrか、JSONの additionalContext です。逆に、コンテキストに入れたくない出力は、入るイベントでは 1>&2 に逃がします。stdoutの行き先は公式リファレンスの「Exit code 0」の節、exit 2のstderrの行き先は「Exit code 2 behavior per event」の表で確認できます。
アンチ3: matcherを "*" に広げる
PreToolUse を全toolに対して走らせると、Read / Grepなどの読み取り系ツールの度に発火して対話が体感で遅くなる典型例です。Bash や Write といった「副作用のあるtool」にmatcherを絞るのが定石です。
アンチ4: フックが起動しないことに気づかない
フックのスクリプトのパスを打ち間違えると、シェルがexit 127などで終わり、非ブロッキングのエラー扱いで処理は進みます。検問のつもりで置いたフックが、黙って無効になる形です。ポリシー用のフックは、最初の1回に画面へ出る hook error の表示を必ず確認します。/hooks メニューで登録内容を見られ、claude --debug でも実行の様子を追えます。
フックを増やすときの3つの指針
本記事のレシピを組み合わせるときの指針は次の3点です。
- hookはゼロから増やす:
settings.jsonに最初は何も書かず、「このタスクを毎回手で打つのが嫌だな」と感じた時点で1つだけ追加する。先に網羅しようとするとdebug地獄になる - 出力の行き先をイベントごとに決める: 使い分けはアンチ2のとおり。診断用の出力は
1>&2に逃がす - hookの中で
git rev-parse --show-toplevelを起点にする: cwdはセッション中に変わりうる(CwdChangedというイベントもある)。絶対パスではなくリポジトリルート相対で書くと移植性が上がる
まとめ
Hooksは手動で繰り返している作業をClaude Codeに組み込む強力な機構ですが、書きすぎると応答性が落ちる性質も持ちます。本記事の9レシピは「効果が大きく、副作用が少ない」観点で選んでいます。アンチパターン4つを避けつつ、自分の作業フローで「毎回やっている地味な仕事」を1つずつ自動化していくのが現実的な始め方です。
フックを置く運用ポリシーは、定期実行の側のClaude Code Routines完全実践ガイドや、常時読み込む情報の置き場を扱うMemory 3層モデルと並べて決めると整理しやすくなります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。