Claude Code Hooksとは — 33イベントの選び方とexit codeの規則
Claude Code Hooksは、ツール実行やセッション開始などの節目で自分のコマンドを必ず走らせる仕組みです。33イベントの一覧、exit codeの意味、動かないときの切り分けをまとめます。
Claude Code Hooksとは、ツール実行の前後・セッション開始・応答完了といった節目で、自分が登録したコマンドをClaude Codeに必ず実行させる仕組みです。登録先は settings.json の hooks キー。イベントの情報はJSONでコマンドの標準入力(stdin)に渡され、終了コード(exit code)で結果を返します。
CLAUDE.mdの指示はClaudeが読んで従うものなので、守られない余地が残ります。Hooksは条件が合えば毎回動きます。整形・遮断・通知のような「忘れてはいけない処理」を任せる場所です。
Hooksでできる4つのこと
遮断する
PreToolUseでツール実行を止められます。.envへの書き込みや危険なコマンドが典型です。書き換える
PreToolUseのupdatedInputで、ツールに渡る引数そのものを差し替えられます。文脈を足す
SessionStartやUserPromptSubmitの出力は、Claudeが読むコンテキストに入ります。副作用を起こす
編集後の整形、テスト実行、完了通知、ログ記録など、Claudeの外側の処理です。
最初の1個を手順どおりに動かしたい場合はClaude Code Hooksの設定方法、用途別のコピペ用レシピはClaude Code Hooks実例カタログが受け持ちます。ここでは、イベントの選び方、設定の置き場所、終了コードの規則、動かないときの切り分けを扱います。
最小の動くHooks — 3階層の設定を読む
Hooksの設定は「イベント → matcherグループ → handler」の3階層です。次の例は、Edit か Write が成功するたびに、編集したファイルのパスをログへ追記します。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path // empty' >> /tmp/hooks-demo.log"
}
]
}
]
}
}PostToolUse がイベント、matcher が対象ツールの絞り込み、hooks 配列の中身がhandlerです。command のhandlerには、イベントのJSONがstdinで届きます。jq はそのJSONから値を取り出しています。
登録できたかは、セッション内の /hooks で見ます。このメニューは閲覧専用で、追加や変更は設定ファイルを直接編集します。
/hooksファイルを書き換えた後は、通常は数秒で反映されます。反映されないときだけ、セッションを再起動します。
設定はどこに書くか — 7つの置き場所
置き場所でスコープと共有範囲が決まります。同じイベントのhookを複数の場所に置くと、すべてが実行されます。
| 置き場所 | スコープ | 共有 |
|---|---|---|
~/.claude/settings.json | スコープ自分の全プロジェクト | 共有不可(自分のマシンだけ) |
.claude/settings.json | スコープそのプロジェクト | 共有可(リポジトリにコミット) |
.claude/settings.local.json | スコープそのプロジェクト | 共有不可(Claude Codeが書き込むときはgitignore対象) |
| 管理ポリシー設定 | スコープ組織全体 | 共有管理者が制御 |
プラグインの hooks/hooks.json | スコーププラグインの有効中 | 共有プラグインに同梱 |
| Skillのfrontmatter | スコープSkill起動後、セッションの終わりまで | 共有Skillファイル内 |
| サブエージェントのfrontmatter | スコープそのサブエージェントの実行中 | 共有エージェントファイル内 |
チームで同じ遮断ルールを使うなら .claude/settings.json です。個人の通知設定なら ~/.claude/settings.json が向きます。同じhandlerを複数の設定ファイルに書いても、実行は1回に統合されます。
管理者が allowManagedHooksOnly を設定した環境では、ユーザー・プロジェクト・ローカル・プラグインのhookはブロックされます。/hooks に「Only hooks from managed settings run here」と出たら、これが原因です。
全hookを一時的に止めたいときは、設定に "disableAllHooks": true を書きます。1回の起動だけ止めるなら、--settings '{"disableAllHooks": true}' を渡します。個別のhookだけを残したまま無効にする方法はありません。
落とし穴
信頼していないフォルダでは、対話とclaude -pで挙動が逆になる
対話セッションでは、ワークスペースの信頼ダイアログを承諾するまで、すべての設定ファイルのhookが保留されます。自分の ~/.claude/settings.json も含みます。
claude -p やAgent SDKのセッションではダイアログが出ず、フォルダは信頼済みとして扱われます。他人のリポジトリに .claude/settings.json のhookがあると、そのまま走ります。実行前に中身を読むか、--settings '{"disableAllHooks": true}' を付けてください。
33イベントの一覧 — 発火のタイミングと止められるか
イベントは33種類です。「止められる」列は、exit code 2が処理を止める効果を持つかどうかを表します。×のイベントは、すでに起きたことを扱うか、止める手段を持たないイベントです。
セッションと設定まわり(12)
| イベント | 発火タイミング | 止められる |
|---|---|---|
SessionStart | 発火タイミングセッションの開始・再開 | 止められる× |
Setup | 発火タイミング--init-only、または -p での --init / --maintenance | 止められる× |
SessionEnd | 発火タイミングセッションの終了 | 止められる× |
InstructionsLoaded | 発火タイミングCLAUDE.mdや .claude/rules/*.md の読み込み | 止められる× |
ConfigChange | 発火タイミングセッション中の設定ファイル変更 | 止められる○(policy_settings を除く) |
CwdChanged | 発火タイミング作業ディレクトリの変更 | 止められる× |
DirectoryAdded | 発火タイミング/add-dir などでディレクトリを追加した後 | 止められる× |
FileChanged | 発火タイミング監視対象ファイルの変更 | 止められる× |
PreCompact | 発火タイミングコンテキスト圧縮の直前 | 止められる○ |
PostCompact | 発火タイミングコンテキスト圧縮の完了後 | 止められる× |
PreModelSwitch | 発火タイミングモデル切り替えの直前 | 止められる○ |
PostModelSwitch | 発火タイミングモデルが切り替わった後 | 止められる× |
1ターンの入出力(6)
| イベント | 発火タイミング | 止められる |
|---|---|---|
UserPromptSubmit | 発火タイミングプロンプト送信後、Claudeが処理する前 | 止められる○ |
UserPromptExpansion | 発火タイミング入力したコマンドがプロンプトに展開される前 | 止められる○ |
MessageDisplay | 発火タイミング応答テキストの表示中 | 止められる×(元の表示のまま) |
Stop | 発火タイミングClaudeの応答完了 | 止められる○(続行させる) |
StopFailure | 発火タイミングAPIエラーでターンが終わったとき | 止められる× |
Notification | 発火タイミングClaude Codeが通知を出したとき | 止められる× |
ツール呼び出し(6)
| イベント | 発火タイミング | 止められる |
|---|---|---|
PreToolUse | 発火タイミングツール実行の直前 | 止められる○ |
PermissionRequest | 発火タイミング権限の判断が要るとき | 止められる×(JSONで拒否する) |
PermissionDenied | 発火タイミングautoモードがツール呼び出しを拒否したとき | 止められる× |
PostToolUse | 発火タイミングツールが成功した後 | 止められる×(stderrはClaudeに届く) |
PostToolUseFailure | 発火タイミングツールが失敗した後 | 止められる×(stderrはClaudeに届く) |
PostToolBatch | 発火タイミング並列ツール呼び出し一式の完了後、次のモデル呼び出しの前 | 止められる○ |
サブエージェント・タスク・外部連携(9)
| イベント | 発火タイミング | 止められる |
|---|---|---|
SubagentStart | 発火タイミングサブエージェントの起動 | 止められる× |
SubagentStop | 発火タイミングサブエージェントの完了 | 止められる○ |
TaskCreated | 発火タイミングTaskCreate でタスクを作るとき | 止められる○(作成を取り消す) |
TaskCompleted | 発火タイミングタスクを完了にするとき | 止められる○ |
TeammateIdle | 発火タイミングエージェントチームのメンバーが待機に入る直前 | 止められる○ |
WorktreeCreate | 発火タイミングworktreeの作成。標準のgit処理を置き換える | 止められる0以外の終了でworktree作成が失敗 |
WorktreeRemove | 発火タイミングWorktreeCreate で作ったworktreeの削除 | 止められる0以外の終了で、ディレクトリが残れば削除が失敗 |
Elicitation | 発火タイミングMCPサーバーが入力を求めたとき | 止められる○(要求を辞退) |
ElicitationResult | 発火タイミングユーザーの応答を返す前 | 止められる○(辞退扱い) |
33種類すべてを覚える必要はありません。まずは PreToolUse・PostToolUse・Stop・Notification・SessionStart の5つで足ります。残りは「その節目で何かしたい」と思ったときに引く索引です。
やりたいことから選ぶ
| やりたいこと | イベント | 要点 |
|---|---|---|
| 編集後に整形・lintする | イベントPostToolUse(Edit|Write) | 要点stdinの tool_input.file_path を整形コマンドへ渡す |
| 危険なコマンドや保護ファイルへの書き込みを止める | イベントPreToolUse | 要点理由をstderrに書いてexit 2 |
| 圧縮で消えた文脈を戻す | イベントSessionStart(compact) | 要点stdoutがClaudeの文脈に入る |
| 完了条件を満たすまで止めない | イベントStop | 要点exit 2、または prompt / agent タイプで判定 |
| 入力待ち・完了を通知する | イベントNotification / Stop | 要点OS通知やWebhookを呼ぶ |
| 権限ダイアログに自動で答える | イベントPermissionRequest | 要点JSONの decision.behavior で応答 |
| 設定変更を監査する | イベントConfigChange | 要点exit 2で反映を止められる |
「止めたいなら Pre*、観察して副作用を起こしたいなら Post* と Stop」が、選ぶときの基本の軸です。
Stop に付ける完了条件の判定は、組み込みの /goal コマンドが同じことをします。公式ドキュメントでは、セッション単位のpromptタイプのStop hookへの近道として紹介されています。設定を書かずに済むなら、まず /goal を試す選択肢があります。
matcherの3つの評価ルール
matcher は、書いた文字の種類で評価方法が切り替わります。
| matcherの書き方 | 評価 | 例 |
|---|---|---|
"*"、空文字、省略 | 評価すべてに一致 | 例そのイベントの全発火 |
英数字・_・-・空白・,・| だけ | 評価完全一致。| か , で列挙 | 例Bash / Edit|Write |
| それ以外の文字を含む | 評価非アンカーのJavaScript正規表現 | 例^Notebook / mcp__memory__.* |
Edit|Write は正規表現ではなく、2つの名前の列挙です。逆に正規表現になった側は部分一致で、Edit.* は NotebookEdit にも当たります。全体一致にしたいときは ^Edit$ と書きます。matcherは大文字小文字を区別するので、bash では Bash に当たりません。
matcherが見るものは、イベントごとに違います。
| イベント | matcherが絞り込む対象 | 値の例 |
|---|---|---|
PreToolUse / PostToolUse / PostToolUseFailure / PermissionRequest / PermissionDenied | matcherが絞り込む対象ツール名 | 値の例Bash、Edit|Write、mcp__.* |
SessionStart | matcherが絞り込む対象開始の経緯 | 値の例startup、resume、clear、compact、fork |
SessionEnd | matcherが絞り込む対象終了の理由 | 値の例clear、resume、logout、prompt_input_exit、other |
PreCompact / PostCompact | matcherが絞り込む対象圧縮のきっかけ | 値の例manual、auto |
SubagentStart | matcherが絞り込む対象エージェントの種類 | 値の例Explore、Plan、カスタムエージェント名 |
FileChanged と StopFailure は、完全一致の文字種がさらに狭く、英数字・_・| だけです。ハイフン・空白・カンマを含めた時点で正規表現として扱われます。
ifでツール名の先まで絞る
if フィールドは、権限ルールと同じ書き方(Bash(git *) や Edit(*.ts))で、引数まで見て発火を絞ります。
{
"type": "command",
"if": "Bash(git *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
}使える場所は、ツール系の5イベント(PreToolUse・PostToolUse・PostToolUseFailure・PermissionRequest・PermissionDenied)に限られます。それ以外のイベントに if を付けたhookは、一度も実行されません。エラーも出ません。
if に書けるルールは1つです。&& や ||、リストは使えません。複数の条件が要るなら、handlerを分けます。コマンドの中身を解析できないときは、条件に関係なくhookが走ります。確実に遮断したい処理は、if ではなく権限システムに任せる設計です。
handlerの5つのtype
handlerの type は5種類です。既定のタイムアウトは種類ごとに違います。
| type | 実行されるもの | 既定のタイムアウト |
|---|---|---|
command | 実行されるものシェルコマンド。stdinにJSONが届く | 既定のタイムアウト600秒 |
http | 実行されるもの指定URLへのPOST(本文がイベントのJSON) | 既定のタイムアウト600秒 |
mcp_tool | 実行されるもの接続済みMCPサーバーのツール | 既定のタイムアウト600秒 |
prompt | 実行されるものClaudeモデルによる1回きりの判定 | 既定のタイムアウト30秒 |
agent | 実行されるものRead・Grep・Globなどを使えるサブエージェントによる検証 | 既定のタイムアウト60秒 |
prompt と agent は、機械的なルールでは書けない判定向けです。モデルは { "ok": true/false } 形式で結果を返します。model を省略すると、Claude Codeがバックグラウンド処理に使うモデルで評価されます。agent は最大50ターンまで動けますが、実験的機能とされ、本番運用では command が勧められています。詳しい使い方はagent hookでテストが通るまで止めない、prompt hookでStopを判断させる、HTTPエンドポイントで受けるにあります。
同じイベントに登録したhookは並列に実行されます。prompt と agent が13件以上あるときは、12件ずつ走ります(同時実行の上限)。
タイムアウトは、イベントによって短く設定されています。
| 対象 | command / http / mcp_tool の既定 |
|---|---|
UserPromptSubmit・PreModelSwitch・PostModelSwitch | command / http / mcp_tool の既定30秒 |
MessageDisplay | command / http / mcp_tool の既定10秒 |
SessionEnd | command / http / mcp_tool の既定全hookで1.5秒を分け合う。個別の timeout を長くすると、最大60秒まで予算が伸びる |
| 上記以外 | command / http / mcp_tool の既定600秒 |
毎プロンプトの前に走る UserPromptSubmit に重い処理を置くと、応答の開始が遅れます。長い処理は async: true でバックグラウンドへ逃がせますが、その場合は結果で遮断も書き換えもできません。
stdin・exit code・JSONの規則
hookが受け取るもの
全イベントに共通するstdinのフィールドは、session_id・transcript_path・cwd・permission_mode・hook_event_name などです。その上に、イベント固有の値が載ります。PreToolUse なら tool_name・tool_input・tool_use_id です。サブエージェント内で発火したhookには、agent_id と agent_type も付き、メインセッションと見分けられます。
イベントごとにJSONの形が違うので、実装前に手元でサンプルを流して確かめると確実です。
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
echo $?exit codeの意味
| exit code | 意味 | 動作 |
|---|---|---|
| 0 | 意味成功 | 動作stdoutのJSONがあれば適用。SessionStart・UserPromptSubmit などではstdoutがClaudeの文脈に入る |
| 2 | 意味ブロッキングエラー | 動作「止められる」イベントでは処理を止め、stderrが理由としてClaudeに返る |
| それ以外 | 意味非ブロッキングエラー | 動作処理は続行。stderrの先頭行が画面に出る |
exit 2の効果は、前掲の33イベントの表のとおりイベントごとに違います。例外が3つあります。
PermissionRequest:exit 2は解釈されず、権限フローが進みます。拒否するときはJSONのhookSpecificOutput.decisionを使いますSessionStart・SubagentStart・SessionEndなど:exit 2でも止まらず、stderrが利用者の画面に出るだけですSetup・Notification・InstructionsLoadedなど:終了コードもstderrも無視されます
落とし穴
exit 1では止まらない
Unixの慣習ではexit 1が失敗ですが、hookでは非ブロッキングエラーで、処理は続行します。ルールを強制するhookは、exit 2 で終えてください。スクリプトのパスを打ち間違えて起動できなかったときも、同じ非ブロッキングエラーです。
起動失敗や失敗時にも止めたい場合は、command と http のhandlerに "onFailure": "block" を付けます(Claude Code v2.1.295以降)。
PreToolUse を止める最小のスクリプトは、次のようになります。
#!/bin/bash
input=$(cat)
command=$(jq -r '.tool_input.command' <<<"$input")
if [[ "$command" == rm* ]]; then
echo "Blocked: rm commands are not allowed" >&2
exit 2
fi
exit 0rm で始まるコマンドは、stderrの文言がClaudeに返ってブロックされます。それ以外はexit 0で、通常の権限フローに進みます。
JSON出力で細かく制御する
exit codeでは「止める」か「何もしない」かの2択です。細かい制御は、exit 0でstdoutにJSONオブジェクトを1つだけ出して行います。
| 目的 | イベント | 書くもの |
|---|---|---|
| ツール実行を許可・拒否・確認・保留 | イベントPreToolUse | 書くものhookSpecificOutput.permissionDecision(allow / deny / ask / defer) |
| ツールの引数を書き換える | イベントPreToolUse | 書くものhookSpecificOutput.updatedInput(入力オブジェクト全体を置き換える) |
| ブロックして理由を返す | イベントUserPromptSubmit・PostToolUse・Stop など | 書くものトップレベルの decision: "block" と reason |
| Claudeの文脈に足す | イベントSessionStart など | 書くものhookSpecificOutput.additionalContext |
| 権限ダイアログに代わりに答える | イベントPermissionRequest | 書くものhookSpecificOutput.decision.behavior(allow / deny) |
| 処理全体を止める | イベント全イベント共通 | 書くものcontinue: false と stopReason |
複数の PreToolUse hookが違う判定を返したときの優先順位は、deny > defer > ask > allow です。updatedInput で同じツールの入力を複数のhookが書き換えると、どの書き換えが採用されるかは保証されません。書き換えるhookは、1つのツールにつき1つに絞ります。
exit 2とJSONを混ぜた場合、exit 2のブロックが優先されます。stdoutに permissionDecision: "allow" を書いても、覆りません。方式はhookごとに1つ選ぶのが安全です。
追加の文脈(additionalContext など)とstdoutは、10,000文字が上限です。超えた分はファイルに保存され、Claudeには保存先のパスと冒頭の2,000文字までのプレビューが渡ります。
よくあるつまずきと切り分け
hookが動かないときの切り分け
- Q/hooks に、そのイベント名で表示されていますか?答えが表示されていない答えがはい
JSONの構文(末尾のカンマやコメントは使えません)と、置き場所を見直します。数秒待っても出ないときはセッションを再起動します。
Only hooks from managed settings run hereと出ている場合は、管理者がallowManagedHooksOnlyを設定しています。 - Qワークスペースの信頼ダイアログを承諾しましたか?答えが承諾していない答えがはい
承諾するまで、設定ファイルのhookはすべて保留されます。新しいリポジトリで「グローバル設定が効かない」ときの典型です。
- Qmatcherの綴りと大文字小文字は、ツール名と一致していますか?答えが一致していない答えがはい
bashではなくBashのように、ツールの正式名で書きます。ifを付けたイベントが、ツール系の5イベントかも確認します。
それでも動かないとき
サンプルJSONをパイプで流してスクリプトを単体で試し、終了コードを見ます。権限が付いていなければ chmod +x します。起動時に出力先を渡す --debug-file でログを取り、別のターミナルから追えます。
動いているのに効かない場合は、次の表で原因を探します。
| 症状 | 原因 | 対処 |
|---|---|---|
| JSONが無視される、パースエラーが出る | 原因シェルのプロファイルが、非対話でも読み込まれて echo を出し、stdoutの先頭が { でなくなる | 対処プロファイルの echo を対話シェル限定にする(if [[ $- == *i* ]]; then ...)。jq でJSONを組み立てる |
jq: command not found | 原因jq が未インストール | 対処インストールするか、PythonやNodeでJSONを読む |
| スクリプトが起動しない | 原因パスが相対、または実行権限がない | 対処"$CLAUDE_PROJECT_DIR"/.claude/hooks/... と書き、chmod +x する。args を付けるとシェルを介さず直接起動する |
PostToolUse でexit 2しても取り消せない | 原因ツールはすでに実行済み | 対処取り消したい処理は PreToolUse に置く。PostToolUse のstderrはClaudeに届くので、失敗を伝える用途には使える |
PreToolUse が時間切れしたのに実行された | 原因タイムアウトしたhookは、遮断の判定を返さない | 対処遮断に使うhookは "onFailure": "block" を付ける |
| Stop hookが何度も続行させる | 原因続行が8回続くと、Claude Codeが次のブロックを上書きしてターンを終える | 対処stdinの stop_hook_active が true なら早めにexit 0する。上限は CLAUDE_CODE_STOP_HOOK_BLOCK_CAP で変更できる |
| WindowsのPowerShell hookで止まらない | 原因Git Bashの有無で既定のシェルが変わる | 対処"shell": "powershell" と "onFailure": "block" を併記する |
Stop hookの無限再発火は/goal Stop hookが止まらない、PowerShellの件はPowerShell hookがexit 2で止まらない問題に、症状別の詳細があります。
よくある質問
HooksとCLAUDE.mdは、どう使い分けますか?
「必ず実行する処理」はHooks、「判断の指針」はCLAUDE.mdです。CLAUDE.mdはClaudeが読んで従う前提なので、解釈の余地があります。Hooksは節目で自動実行されます。
bypassPermissionsモードでもHooksは効きますか?
効きます。PreToolUse は権限モードの確認より前に発火します。permissionDecision: "deny" を返すと、bypassPermissions や --dangerously-skip-permissions でもツールが止まります。逆に、hookの allow で設定側のdenyルールを覆すことはできません。
サブエージェントの中のツール実行にも発火しますか?
発火します。設定ファイルやプラグインのhookは、サブエージェント内でも同じように走ります。stdinの agent_id と agent_type で見分けます。サブエージェントの仕組みはSub-agents完全ガイドにあります。
hookからスラッシュコマンドやツールを呼べますか?
command のhookは、stdout・stderr・exit codeだけで結果を返します。スラッシュコマンドもツール呼び出しも起動できません。Claudeに伝えたいことは、additionalContext として文脈に足します。
settings.json全体の書き方は、どこで確認できますか?
hooks 以外のキーも含めた全体は、settings.jsonの完全ガイドで扱っています。Claude Code自体の位置付けはClaude Codeの全体像から辿れます。
まとめ
Hooksを設計する手順は、3つの問いに落ちます。止めたいのか(Pre* とexit 2)、観察して副作用を起こしたいのか(Post*・Stop・Notification)、機械的なルールで書けない判定なのか(prompt / agent)です。
最初の1個は、PostToolUse の整形か PreToolUse の保護ファイル遮断が、効果と失敗の少なさの両面で扱いやすい選択です。動いたら、実例カタログから自分の運用に近いレシピを足していけます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。