Claude Codeのフック(hooks)は、設定ファイルに書いたシェルコマンドなどを、セッション開始時やツール呼び出しの前後といった決まった時点で自動実行する仕組みです。結論から言うと、覚えることは三つだけです。どのイベントで動かすか、どのマッチャーで絞るか、どのハンドラーを走らせるか。この三階層をJSONで入れ子にすれば、最小構成のフックは数行で書けます。
フックが向いているのは、「編集したら整形してほしい」「危ないコマンドは止めてほしい」「セッションの最初にブランチ名を教えてほしい」といった、毎回同じ手順でやりたい作業です。プロンプトでお願いする方法だと、その時のモデルの判断に左右されます。フックはClaude Codeのライフサイクルに直接ぶら下がるため、条件が合えばハンドラーが起動します。公式ガイドも、フックの価値を決定論的な制御(deterministic control)として説明しています(Automate actions with hooks)。
この記事では、公式リファレンスに実在するイベント名・設定キー・マッチャー書式だけを使って、設定ファイルの書き方、イベントの選び方、入力JSONの読み方、終了コードとJSON出力の使い分け、そして動かないときの切り分け手順までを順に解説します。仕様の根拠は各段落に公式ページのリンクを置いています。Claude Code自体のインストールや基本操作がまだの方は、先にClaude Codeの使い方(インストールと5つの機能)を読んでから戻ってくると、設定ファイルの話が具体的に見えるはずです。
Claude Codeのフックとは何か|LLMの判断に任せない自動処理
【完全無料】AI×SNSで成果を出す100大特典を受け取る
AIの使い方を、SNS運用の実務につなげたい方へ。LINEで特典の案内を受け取れます。
フックとは、Claude Codeのライフサイクル上の特定のポイントで自動的に実行される、ユーザー定義の処理のことです。設定ファイルにイベント名と実行したい内容を書くだけで登録でき、モデルへの依頼文とは独立して動きます。
公式リファレンスの冒頭は、フックを「シェルコマンド、HTTPエンドポイント、MCPツール呼び出し、LLMプロンプト、またはサブエージェント」と説明しています(Hooks reference)。つまりフックの正体は「自動で走る何か」であって、シェルスクリプト専用の仕組みではありません。同じページには、ターミナルのセッション、IDE拡張、デスクトップアプリ、クラウドセッションのいずれでも同じイベントが発火する、という記述もあります。
イベントやフィールドには、対応する Claude Code のバージョンが公式に明記されているものがあります。たとえば PreModelSwitch と PostModelSwitch は v2.1.251 以降、コマンドハンドラーの onFailure は v2.1.295 以降が必要です(Hooks reference)。書いても動かないときは、手元の Claude Code が古くないかも疑ってください。この記事の内容は2026年10月18日に公式ページで確認したものです。
使いどころを具体的に言うと、次のような場面です。ファイルを編集した直後にフォーマッタやリンタを走らせる。特定のシェルコマンドをツール実行前に止める。Claudeが応答を終えたタイミングでデスクトップ通知を出す。セッション開始時に現在のブランチや未コミットの差分を文脈として流し込む。いずれも「Claudeに毎回お願いする」運用から「設定に書いておけば起動する」運用へ変える作業です。大規模な開発でこの決定論的な足場がどう効くかは、Claude Code Dynamic Workflowsと大規模開発の解説と合わせて読むと立体的に理解できます。
逆に、フックに向かない仕事もあります。内容が毎回変わる判断、文章の良し悪しの評価、その場の相談などは、依頼文やスキルの領域です。Claude Codeの全体像や料金まわりを整理したい場合は、Claude活用の使い方(料金・モデル・始め方)も参考にしてください。
フック設定の三階層|イベント・マッチャー・ハンドラー
フックの設定は、イベント名・マッチャーグループ・ハンドラーという三階層のJSONで表現します。公式リファレンスも「設定には三段階の入れ子がある」と説明しており、この形は設定ファイル・プラグイン・スキルのいずれでも共通です。
公式のHooks referenceによると、書く順番は「①反応したいイベントを選ぶ、②発火条件を絞るマッチャーグループを足す、③マッチしたときに走るハンドラーを一つ以上定義する」です。JSONに落とすと次の形になります。ファイルを編集・新規作成したときだけ整形スクリプトを呼ぶ例です。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "/path/to/lint-check.sh"
}
]
}
]
}
}
外側の hooks はイベント名を並べるための箱です。その中の PostToolUse が配列になっていて、各要素が「マッチャー+ハンドラー群」の組です。さらに内側の hooks 配列に、実際に走る処理を並べます。名前が同じで紛らわしいので、外側は「イベントの箱」、内側は「ハンドラーの箱」と読み替えると迷いません。
すでに設定ファイルに hooks キーがある場合、公式ガイドは「既存のイベントキーと並べて兄弟として追加する」よう案内しています(Automate actions with hooks)。hooks オブジェクトごと上書きすると、前に書いたイベントが消えます。追記するときはイベント名のキーを足す、と覚えておくと事故が減ります。
同じイベントに複数のハンドラーが一致した場合、公式リファレンスは「マッチしたフックはすべて並列に実行される」と説明し、すべてのコマンドが終わってから結果が統合されると書いています。片方が deny を返しても、もう片方の処理は止まりません。ログ書き込みのようなフックと、拒否を返すフックを同居させるときは、この点を前提に設計します(Automate actions with hooks)。同じツールの入力を複数のフックが updatedInput で書き換えると、最後に終わったものが有効になり順序が不定になるため、書き換えは一つのフックに任せます。
設定ファイルはどこに書くか|置き場所と適用範囲
フックを書く場所は、ユーザー設定・共有プロジェクト設定・プロジェクトローカル設定・管理ポリシー設定・プラグイン・スキル・サブエージェントの各所です。どこに置いたかで、適用される範囲とチームへ共有できるかが変わります。
公式リファレンスの一覧を、置き場所・適用範囲・共有可否の三列に整理すると次のとおりです(出典はHooks referenceのHook locations)。
| 置き場所 | 適用範囲 | 共有できるか |
|---|---|---|
~/.claude/settings.json |
自分のすべてのプロジェクト | 共有されない(自分のマシン内) |
.claude/settings.json |
そのファイルがあるプロジェクト | リポジトリにコミットすれば共有できる |
.claude/settings.local.json |
そのプロジェクトの自分だけ | 共有されない(Claude Codeが設定を保存するとgit管理外になる) |
| 管理ポリシー設定 | 組織全体 | 共有できる(管理者が制御する) |
プラグインの hooks/hooks.json |
そのプラグインが有効な間 | プラグインに同梱して配れる |
| スキルのフロントマター | スキルを呼び出した後のセッション | スキルファイルに書いて配れる |
| サブエージェントのフロントマター | そのサブエージェントの実行中 | サブエージェントの定義ファイルで配れる |
チームで同じ検査を回したいなら .claude/settings.json、自分の手元だけで試すなら .claude/settings.local.json、どのプロジェクトでも通知を出したいなら ~/.claude/settings.json、という選び分けになります。一覧は公式リファレンスのHook locationsにあります。
全部を一時的に止めたいときは、設定ファイルに "disableAllHooks": true を書きます。公式リファレンスによると、Claude Code は設定の優先順位を適用した後に残った値を読むため、プロジェクトの .claude/settings.json に "disableAllHooks": false があると、ユーザー設定の true を上書きします。その実行だけ切りたいときは --settings '{"disableAllHooks": true}' を渡します。個別のフックを設定に残したまま無効化する方法は用意されていません(Disable or remove hooks)。
管理ポリシーで配られたフックは、ユーザー・プロジェクト・ローカルの disableAllHooks では止められず、止められるのは管理設定側で disableAllHooks を指定したときだけです。設定ファイルのフックを直接編集した場合は、通常はファイル監視が自動で拾います。設定ファイルが変わったときに動く ConfigChange イベントもあり、設定変更の監査に使えます。
イベント一覧|公式の全33イベントを発火タイミング順に
フックイベントとは、Claude Codeがフックを起動する時点そのものです。2026年10月18日時点の公式リファレンスには33のイベントが載っており、セッション単位・ターン単位・ツール呼び出し単位の三つのケイデンスに分かれて発火します。
まず大枠です。Hooks referenceは、セッションごとに一回なのが SessionStart と SessionEnd、ターンごとに一回なのが UserPromptSubmit・Stop・StopFailure、エージェンティックループの中でツール呼び出しのたびに動くのが PreToolUse と PostToolUse だと整理しています。ただし EndConversation ツールの呼び出しだけは、この二つのイベントを飛ばします。
次の表が、公式リファレンスのイベント一覧とExit code 2 behavior per eventの表をひとつにまとめたものです。マッチャーの列は、matcher に書いた値が何と照合されるかを表します。「非対応」のイベントでは matcher を書いても無視され、常に発火します。
| イベント名 | いつ発火するか | マッチャーの照合先 | 終了コード2で止められるか |
|---|---|---|---|
SessionStart |
セッションの開始時と再開時 | startup / resume / clear / compact / fork | 止められない(stderrは利用者に表示) |
Setup |
--init-only、または -p モードの --init / --maintenance で起動したとき |
init / maintenance | 止められない(終了コードとstderrは無視) |
UserPromptSubmit |
入力を送信した直後、Claudeが処理する前 | 非対応 | 止められる(プロンプトはClaudeに届かない) |
UserPromptExpansion |
利用者が入力したコマンドがプロンプトに展開されるとき | コマンド名 | 止められる(展開をブロック) |
PreToolUse |
ツール呼び出しの実行前 | ツール名 | 止められる(ツール呼び出しをブロック) |
PermissionRequest |
ツール呼び出しに許可の判断が要るとき | ツール名 | 止められない(decision オブジェクトで拒否する) |
PermissionDenied |
自動モードがツール呼び出しを拒否したとき | ツール名 | 止められない(拒否は済んでいる) |
PostToolUse |
ツール呼び出しが成功した後 | ツール名 | 止められない(stderrがClaudeに渡る) |
PostToolUseFailure |
ツール呼び出しが失敗した後 | ツール名 | 止められない(stderrがClaudeに渡る) |
PostToolBatch |
並列ツール呼び出しの束が解決した後 | 非対応 | 止められる(次のモデル呼び出し前でループを停止) |
Notification |
Claude Codeが通知を出すとき | permission_prompt、idle_prompt、auth_success など |
止められない(終了コードとstderrは無視) |
MessageDisplay |
アシスタントのメッセージ文が表示されている間 | 非対応 | 止められない(元の文が表示される) |
SubagentStart |
サブエージェントが起動したとき | エージェントの種類 | 止められない(stderrは利用者に表示) |
SubagentStop |
サブエージェントが終了したとき | エージェントの種類 | 止められる(サブエージェントの停止を防ぐ) |
TaskCreated |
TaskCreate でタスクが作られたとき |
非対応 | 止められる(作成を取り消す) |
TaskCompleted |
タスクが完了にされたとき | 非対応 | 止められる(完了扱いにしない) |
Stop |
Claudeが応答を終えたとき | 非対応 | 止められる(停止させず会話を続ける) |
StopFailure |
APIエラーでターンが終わったとき | rate_limit、overloaded などのエラー種別 |
止められない(出力と終了コードは無視) |
TeammateIdle |
エージェントチームの仲間が待機に入ろうとしているとき | 非対応 | 止められる(作業を続けさせる) |
InstructionsLoaded |
CLAUDE.md や .claude/rules/*.md が読み込まれたとき |
session_start、nested_traversal、path_glob_match、include、compact |
止められない(終了コードは無視) |
ConfigChange |
セッション中に設定ファイルなどが変わったとき | user_settings、project_settings、local_settings、policy_settings、skills |
止められる(policy_settings を除く) |
CwdChanged |
作業ディレクトリが変わったとき | 非対応 | 止められない(stderrは利用者に表示) |
DirectoryAdded |
/add-dir などで作業ディレクトリが追加されたとき |
slash_command、register_repo_root |
止められない(stderrはデバッグログへ) |
FileChanged |
監視しているファイルがディスク上で変わったとき | ファイル名(.envrc|.env のように並べる) |
止められない(stderrは利用者に表示) |
WorktreeCreate |
ワークツリーが作られるとき | 非対応 | 止められる(0以外の終了コードで作成が失敗) |
WorktreeRemove |
ワークツリーが片付けられるとき | 非対応 | 止められる(ディレクトリが残る場合、0以外で削除が失敗) |
PreCompact |
コンテキストの圧縮前 | manual / auto |
止められる(圧縮をブロック) |
PostCompact |
コンテキストの圧縮が終わった後 | manual / auto |
止められない(stderrは利用者に表示) |
PreModelSwitch |
要求されたモデル切替が適用される前(v2.1.251以降) | 正規のモデル名 | 止められる(切替をブロック) |
PostModelSwitch |
セッションのモデルが変わった後(v2.1.251以降) | 正規のモデル名 | 止められない(切替は済んでいる) |
Elicitation |
MCPサーバーが利用者に入力を求めたとき | MCPサーバー名 | 止められる(要求を辞退し、ダイアログは出ない) |
ElicitationResult |
利用者がMCPの入力要求に答えた後 | MCPサーバー名 | 止められる(応答を辞退扱いにする) |
SessionEnd |
セッションの終了時 | clear / resume / logout / prompt_input_exit / other |
止められない(stderrは利用者に表示) |
イベント名は大文字小文字まで公式ページの表記どおりに書いてください。表の名前以外を書いたエントリは意図どおりに動かないため、/hooks で登録を確認する習慣をつけると安全です。
選び方の目安はシンプルです。ツールの実行を止めたいなら PreToolUse、実行結果に反応したいなら PostToolUse、入力を検査したいなら UserPromptSubmit、終了時に片付けたいなら SessionEnd、開始時に文脈を足したいなら SessionStart です。設定ファイルが変わったことを検知したいなら ConfigChange、特定のファイルの変更に反応したいなら FileChanged が向いています。
マッチャーの書き方|完全一致と正規表現の分かれ目
マッチャーとは、イベントが発火したときにそのハンドラーを走らせるかどうかを決める文字列です。書いた文字の種類によって、完全一致として扱われるか、正規表現として扱われるかが変わります。
公式リファレンスの評価規則は次の三通りです。この分岐を知らないと「なぜか関係ないツールでも動く」という現象に当たります。
| マッチャーの書き方 | 評価のされ方 | 具体例 |
|---|---|---|
"*"、空文字、または省略 |
すべてにマッチ | そのイベントが起きるたびに発火する |
英数字・_・-・スペース・,・| だけ |
完全一致、または区切りで並べた完全一致のリスト | Bash はBashツールだけ、Edit|Write はどちらかのツール |
| それ以外の文字を含む | JavaScriptの正規表現(前後は固定されない) | ^Notebook、mcp__memory__.* |
三段目が落とし穴です。公式リファレンスは「Edit.* は Edit にも NotebookEdit にもマッチする」「全体一致にしたいなら ^Edit$ のように ^ と $ で囲む」と明記しています(Matcher patterns)。ドットやアスタリスクを一文字入れた瞬間に、意味が完全一致から部分一致へ変わる、と理解しておくと安全です。
もう一つ重要なのは、マッチャーが何を見るかがイベントごとに違う点です。PreToolUse・PostToolUse・PostToolUseFailure・PermissionRequest・PermissionDenied はツール名を見ます。SessionStart はセッションの始まり方を見て、値は startup・resume・clear・compact・fork です。SessionEnd は終わった理由で、clear・resume・logout・prompt_input_exit・other が並びます。PreCompact と PostCompact は manual か auto です。FileChanged は特殊で、| で区切った各語が「監視するファイル名」として登録され、".envrc|.env" なら二つのファイルを見張ります。正規表現を書いても期待した監視にはなりません。
そして、マッチャーを受け付けないイベントもあります。公式リファレンスの表では UserPromptSubmit、PostToolBatch、MessageDisplay、TaskCreated、TaskCompleted、Stop、TeammateIdle、CwdChanged、WorktreeCreate、WorktreeRemove が「マッチャー非対応」に分類され、常に発火します。非対応イベントに matcher を書いても、エラーにはならず黙って無視されます。
MCPサーバーのツールも、通常のツールと同じようにツール名でマッチできます。名前の形は mcp__<server>__<tool> で、あるサーバーの全ツールに当てたいときは mcp__memory__.* のように末尾へ .* を付けます。MCP自体の考え方はMCPとは(Claudeを外部ツールにつなぐ仕組み)にまとめています。
ツール名よりさらに細かく絞りたいときは、ハンドラー側の if フィールドを使います。書式は権限ルールと同じで、"Bash(git *)" ならBash入力のサブコマンドが git * に一致したときだけ、"Edit(*.ts)" ならTypeScriptファイルのときだけハンドラーが起動します。ただし公式リファレンスは、if が評価されるのはツールイベントだけで、それ以外のイベントに if を書いたハンドラーは一度も走らないと明記しています。加えて、if には権限ルールをちょうど一つだけ書けます。条件を二つ重ねたいときは、ハンドラーを二つ定義します。なお、この絞り込みは完全な保証ではありません。公式リファレンスは、if がコマンドを判別できないときにフックを走らせることがあると注意しており、確実な許可・拒否は権限システムで決めるよう案内しています。
ハンドラーの5種類|command・http・mcp_tool・prompt・agent
ハンドラーとは、マッチャーが一致したときに実際に走る処理の定義です。内側の hooks 配列に並べたオブジェクトがそれぞれハンドラーで、type によって実行のされ方と結果の返し方が変わります。
公式リファレンスのHook handler fieldsに並ぶ五種類を、実行内容と結果の返し方で整理します。
type |
実行するもの | 結果の返し方 |
|---|---|---|
command |
シェルコマンド | 標準入力でJSONを受け取り、終了コードと標準出力で返す |
http |
指定URLへのPOSTリクエスト | リクエストボディで受け取り、レスポンスボディのJSONで返す |
mcp_tool |
接続済みMCPサーバーのツール呼び出し | ツールのテキスト出力が、コマンドの標準出力と同じ扱いになる |
prompt |
Claudeモデルによる単発の評価 | モデルが決められた形のJSONで判定を返す |
agent |
ツールを使えるサブエージェントによる検証 | サブエージェントが判定を返す(実験的な機能) |
command ハンドラーは、command に加えて args・async・asyncRewake・shell("bash" または "powershell")・onFailure を受け取ります。ここで押さえたいのが、公式リファレンスの言うexec形式とshell形式の違いです。args を書くとexec形式になり、command は実行ファイルとして直接起動されます。シェルを経由しないので、引用符やドル記号の解釈で悩まなくて済みます。args を省くとshell形式になり、文字列がシェルへ渡るため、パイプや && が使えます。パスのプレースホルダを使う場合はexec形式が推奨されています。
そのプレースホルダは三つあります。${CLAUDE_PROJECT_DIR} がセッションを開始したプロジェクトのルート、${CLAUDE_PLUGIN_ROOT} がプラグインのインストール先、${CLAUDE_PLUGIN_DATA} がプラグインの永続データ用ディレクトリです。スクリプトを相対パスで書くとフックの起動時に見つからないことがあるため、公式のベストプラクティスもフルパスでの指定を勧めています。
公式のHTTP hook fieldsによると、http ハンドラーは url が必須で、headers と allowedEnvVars を足せます。ヘッダー値に環境変数を差し込むには allowedEnvVars にその変数名を並べる必要があり、載せていない変数への参照は空文字に置き換わります。HTTPハンドラーもコマンドと同じ入力JSONをPOSTのボディとして受け取り、制御はレスポンスのJSONで返します。
mcp_tool ハンドラーは server と tool が必須で、input に "${tool_input.file_path}" のような参照を書けます。設定済みのMCPサーバーのツールを呼ぶ形なので、サーバー側の接続が前提になります。
prompt ハンドラーは、判断そのものをClaudeモデルに任せる形です。prompt にテキストを書き、入力JSONを差し込みたい位置に $ARGUMENTS を置きます。モデルは ok と reason を持つJSONで応答し、ok: false のときは reason が返ります。Stop と SubagentStop では、モデルが impossible: true を付けると条件が満たせないものとして停止が許可されます。使うモデルは model フィールドで指定でき、判定文のつくり方はClaudeのプロンプトのコツの考え方がそのまま使えます。内容が毎回変わる判断はこの形、決まった規則ならコマンドで書く、と使い分けます。
agent ハンドラーはツールを使って検証できる代わりに、公式リファレンス自身が実験的で変更されうると断っています。本番のワークフローではコマンドハンドラーを優先する、という案内も同じページにあります。サブエージェントの仕組みはClaude Codeのサブエージェント使い方で詳しく扱っています。
タイムアウトの既定値も種類で違います。公式リファレンスの共通フィールド表によると、command・http・mcp_tool は600秒、prompt は30秒、agent は60秒です。さらに UserPromptSubmit・PreModelSwitch・PostModelSwitch では前者三つの既定が30秒へ、MessageDisplay では10秒へ下げられます。SessionEnd は全体で1.5秒の予算を共有し、設定でより長い timeout を書くと最大60秒まで予算が引き上げられます。長い処理を入口イベントに置くと入力がもたつくため、重い処理は後段のイベントか非同期実行に寄せます。
フックが受け取る入力JSON|stdinから渡る共通フィールド
フックの入力JSONとは、イベントが発火したときにClaude Codeがハンドラーへ渡すデータです。コマンドハンドラーには標準入力から、HTTPハンドラーにはPOSTのリクエストボディとして届きます。
どのイベントでも共通して入ってくるフィールドは、Hooks referenceのCommon input fieldsに一覧があります。実務で使う頻度が高いのは次のあたりです。
session_id:現在のセッション識別子。ログを突き合わせるときの鍵になりますtranscript_path:会話JSONのパス。非同期で書かれるため、直前のメッセージがまだ反映されていない場合があると明記されていますcwd:フックが呼ばれた時点の作業ディレクトリpermission_mode:現在の権限モード。default・plan・acceptEdits・auto・dontAsk・bypassPermissionsのいずれかhook_event_name:発火したイベント名prompt_id・scratchpad_dir・effort:処理中のプロンプトの識別子、作業用ディレクトリ、実行時のエフォートレベル
サブエージェントの中で発火した場合は agent_id と agent_type が足されます。メインの会話から呼ばれたのか、サブエージェントの中なのかを見分けたいときは、この二つを見ます。
イベント固有のフィールドも入ります。たとえば PreToolUse なら tool_name・tool_input・tool_use_id が加わります。公式リファレンスに載っているBashツールの例は、次の形です。
{
"session_id": "abc123",
"cwd": "/home/user/my-project",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test",
"description": "Run test suite"
},
"tool_use_id": "toolu_01ABC123..."
}
この構造が分かれば、スクリプト側の書き出しはいつも同じです。標準入力を読み、必要なキーを取り出す。シェルなら jq を使うのが公式の例でも定番です。
#!/bin/bash
input=$(cat)
command=$(jq -r '.tool_input.command' <<<"$input")
file=$(jq -r '.tool_input.file_path // empty' <<<"$input")
作業ディレクトリの扱いには一つ注意点があります。公式リファレンスは、Claudeがワークツリーへ入っても ${CLAUDE_PROJECT_DIR} はセッション開始時のプロジェクトルートを指したままだと説明しています。一方、入力JSONの cwd はフックが呼ばれた時点の作業ディレクトリです。スクリプトの置き場所を指すなら前者、Claudeがいま触っているディレクトリを知りたいなら後者、と役割を分けて使います。
終了コードの決まり|0・2・それ以外で挙動が変わる
終了コードとは、コマンドハンドラーが終了時に返す数値です。Claude Codeはこの値を見て、処理を進めるか、止めるか、エラーとして扱うかを決めます。
公式リファレンスのExit code outputの節を整理すると、次の三通りです。
| 終了コード | 意味 | 標準出力・標準エラーの扱い |
|---|---|---|
0 |
成功。構造化された制御をしたいときもこの値で返す | 標準出力が全体で一つのJSONオブジェクトならJSONとして解釈され、そうでなければ平文として扱われる。平文の標準出力が文脈として渡るのは UserPromptSubmit・UserPromptExpansion・SessionStart・PostModelSwitch だけ。Claudeはstderrを見ない |
2 |
ブロッキングエラー。止められるイベントでは動作を止める | 標準エラーの文言が理由としてClaudeに返る。効果はイベントごとに異なる |
| それ以外 | 多くのイベントでは単独ではブロックしない | 非ブロッキングエラー。動作はそのまま進み、stderrの最初の1行が「hook error」の通知として表示される(onFailure: "block" を設定した場合を除く) |
ここが最初のつまずきどころです。0と2以外の終了コードは、onFailure: "block" を設定していない限り非ブロッキングエラーとして扱われ、動作はそのまま進みます。Unixの慣習で1を返すスクリプトをそのままフックにすると、止めたつもりが素通りします。ポリシーを強制したいなら exit 2 を使います(Hooks reference)。
終了コード2の効果はイベントで変わります。PreToolUse はツール呼び出しをブロック、UserPromptSubmit はプロンプトをブロックしてClaudeに届かせない、Stop はClaudeの停止を止めて会話を続ける、PreCompact は圧縮をブロックします。イベントごとの効果は前節の表の右端の列にまとめています。一方で PostToolUse は「ツールがもう走ってしまっている」ため止められず、標準エラーの内容がClaudeへ見えるだけです。Notification に至っては終了コードもstderrも無視されます。
実装の骨格は、公式リファレンスの例と同じくこの形で足ります。rm で始まるコマンドだけを止め、それ以外は通常の権限フローへ返す仮の例です。
#!/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 0
失敗したときの扱いにも触れておきます。公式リファレンスによると、多くのイベントではフックが失敗またはタイムアウトしても、Claude Codeは元の動作を続けます。パスの間違いやスクリプトの異常終了で、ポリシー用のフックが全部を素通しにする恐れがあるということです。command または http のフックに "onFailure": "block" を設定すると、失敗時に動作を止められます(Claude Code v2.1.295以降。Stop・SubagentStop・TaskCompleted・TeammateIdle と、async を使うバックグラウンドのフックには効果がありません)。止まったフックを関門として当てにするなら、この設定を検討します。
JSON出力で細かく制御する|許可・拒否・文脈追加・出力の書き換え
JSON出力とは、ハンドラーが標準出力へ書くJSONオブジェクトのことです。終了コードだけでは表現できない、許可・拒否・利用者への確認・文脈の追加・入出力の書き換えを指定できます。
どのイベントでも受け付ける汎用フィールドから確認します。continue を false にするとフックの実行後にClaudeが処理を打ち切り、stopReason がそのとき利用者へ表示されます。systemMessage は警告として利用者に見せる文字列、terminalSequence は許可されたエスケープシーケンスをClaude Code側から出してもらうためのフィールドです。suppressOutput は受け付けられるものの効果はない、と公式リファレンスが明記しています。出力文字列は additionalContext なども含めて10,000文字が上限で、超えた分はファイルへ書き出されてプレビューとパスに置き換わります。
Claudeへ情報を足したいときは hookSpecificOutput の中に hookEventName と additionalContext を書きます。この文字列はシステムリマインダーとして会話へ差し込まれ、チャットのメッセージとしては表示されません。公式リファレンスは、命令文ではなく事実の記述として書くことを勧めています。命令の形にすると、プロンプトインジェクション対策の側に引っかかって、Claudeが文脈として扱わず利用者へ見せることがあるためです。
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "This file is generated. Edit src/schema.ts instead."
}
}
決定の書き方はイベントによって二系統に分かれます。主要なものを並べます。
| イベント | 決定の書き方 | 主なキー |
|---|---|---|
PreToolUse |
hookSpecificOutput の中 |
permissionDecision(allow / deny / ask / defer)、permissionDecisionReason、updatedInput、additionalContext |
PostToolUse |
トップレベル(updatedToolOutput と additionalContext は hookSpecificOutput の中) |
decision: "block"、reason、updatedToolOutput |
UserPromptSubmit |
トップレベル | decision: "block"、reason |
Stop・SubagentStop |
トップレベル | decision: "block"、reason(ブロック時は必須) |
SessionStart |
hookSpecificOutput の中(文脈のみ) |
additionalContext、initialUserMessage、sessionTitle、watchPaths、reloadSkills |
PreToolUse は許可判断を hookSpecificOutput.permissionDecision に書きます。値は allow・deny・ask・defer の四つで、hookSpecificOutput には hookEventName が必要です。
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Database writes are not allowed"
}
}
書く位置を間違えやすい点も押さえておきます。公式ガイドによると、permissionDecision や additionalContext をトップレベルに置くと、JSONとしては解釈されるものの、エラーなしで無視されます。PostToolUse や Stop の decision と reason はトップレベルに書くので、イベントごとに置き場所が違うと覚えてください。
書き換え系のキーも覚えておくと応用が利きます。PreToolUse の updatedInput はツールの引数を実行前に差し替えます。オブジェクト全体を置き換える形なので、変更しないフィールドも一緒に入れます。PostToolUse の updatedToolOutput はツールの結果を差し替えますが、公式リファレンスが警告しているとおり、変わるのはClaudeに見える内容だけです。ファイル書き込みやコマンド実行はすでに済んでいます。実行そのものを変えたいなら PreToolUse 側で止めます。
権限モードとの関係も整理しておきます。公式ガイドによると、PreToolUse のフックはどの権限モードでも権限チェックの前に発火し、permissionDecision: "deny" は bypassPermissions や --dangerously-skip-permissions でもツールを止めます。利用者が権限モードを変えても回避できないポリシーを置けるということです。
最初の1本を作る手順(仮の例)|編集後にフォーマッタを走らせる
最初のフックは、編集直後に整形コマンドを走らせる PostToolUse が扱いやすい題材です。失敗しても会話が止まらず、効果がファイルの差分として目に見えるため、設定が届いているかを確認しやすいからです。
ここから先は、すべて仮の例です。実在のプロジェクトの数値や成果ではなく、手順を確かめるための構成として読んでください。進め方は次の順番です。
- 設定ファイルに
hooksブロックを書く /hooksで登録されたか確認する- スクリプトを置いて実行権限を付ける
- 実際にClaudeへ編集を頼み、結果を確認する
まず、プロジェクトの .claude/settings.json に次を書きます。args を付けてexec形式にし、${CLAUDE_PROJECT_DIR} でスクリプトの場所を指しています。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format-after-edit.sh",
"args": [],
"timeout": 30,
"statusMessage": "formatting"
}
]
}
]
}
}
次に .claude/hooks/format-after-edit.sh を置きます。編集されたファイルのパスを受け取り、拡張子で分岐して整形コマンドを呼ぶだけの内容です。整形対象でなければ何もせずに終了コード0で抜けます。
#!/bin/bash
input=$(cat)
file=$(jq -r '.tool_input.file_path // empty' <<<"$input")
case "$file" in
*.ts|*.tsx|*.js) npx prettier --write "$file" >/dev/null 2>&1 ;;
*) exit 0 ;;
esac
jq -nc --arg f "$file" '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: ("formatted " + $f)}}'
保存したら chmod +x .claude/hooks/format-after-edit.sh で実行権限を付けます。公式ガイドのトラブルシュートでも、スクリプトが動かない原因としてこの実行権限が挙がっています。
登録の確認は /hooks です。Claude Codeのプロンプトで入力するとフックのブラウザが開き、登録したフックがイベント名の下に表示されます(Automate actions with hooks)。追加や修正は設定JSONを直接編集して行います。
二本目として、危険なコマンドを止める PreToolUse を足す場合はこうなります。matcher でBashに絞り、さらに if で rm * に一致するサブコマンドだけへ絞ることで、無関係なコマンドのたびにプロセスを起動しなくて済みます。これも仮の例です。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
"args": []
}
]
}
]
}
}
公式リファレンスは、この構成で Bash "rm -rf /tmp/build" が来たときの流れを段階で示しています。イベントが発火し、マッチャーがツール名で一致し、if がサブコマンドで一致し、ハンドラーが起動して permissionDecision に deny を返し、Claude Codeが呼び出しをブロックする、という順番です。何も返さずに終了コード0で抜けた場合は「判断なし」で、通常の権限フローへ戻ります。黙っていることは許可ではない、という点は設計上の重要な前提です。
時間のかかる処理を足したくなったら、async を true にする選択肢があります。バックグラウンドで走るためClaudeは待たされず、結果は次のターンで届きます。ただし公式ガイドによると、非同期フックの出力が届くのは次の会話ターンです。テストの実行結果を知らせるような用途向けで、その場で動作を止める関門には向きません。
動かないときの切り分け|/hooksとデバッグログで原因を絞る
フックの不具合は、登録されていない、起動に失敗している、JSONが解釈されていない、の三つに大別できます。公式ガイドのトラブルシュートは、この順に確認する手順を示しています。
まず「そもそも発火していない」ケースです。/hooks を開いて、目的のイベントの下に出ているかを確認します。出ていなければ設定ファイルの場所かJSONの妥当性の問題です。公式ガイドは、末尾のカンマやコメントは許されないこと、ファイル監視が変更を取りこぼした場合はセッションを開始し直すと読み込み直せることを挙げています。名前の綴りや位置を間違えてもエラーが出ないことがあるため、登録できたかは自分で確かめます。
登録されているのに走らない場合は、マッチャーを疑います。公式ガイドは明確に、マッチャーは大文字と小文字を区別すると書いています。bash と Bash は別物です。イベントの取り違えも定番で、PreToolUse は実行前、PostToolUse は実行後に発火します。
「フックエラー」が会話に出る場合は、スクリプト側です。手元で同じ入力を流して再現するのが早道で、公式ガイドも次の確かめ方を示しています。
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
echo $?
よくある原因は三つあります。パスが通っていないならフルパスか ${CLAUDE_PROJECT_DIR} を使い、引用の問題を避けたいなら "args": [] を足してexec形式にします。jq: command not found なら jq を入れるか、PythonやNode.jsでJSONを解析します。実行権限が付いていないなら chmod +x です。
いちばん分かりにくいのが「JSONを出しているのに効かない」ケースです。公式ガイドは原因を二つ挙げています。一つはJSONの前に余計な出力が混ざる場合で、シェルの設定ファイルにある無条件の echo が典型です。出力が { で始まらなくなるため、全体が平文として扱われます。対策は、シェルの設定側で対話シェルのときだけ出力するように囲うことです。
if [[ $- == *i* ]]; then
echo "Shell ready"
fi
もう一つはフィールドの階層違いです。permissionDecision はトップレベルではなく hookSpecificOutput の中に置きます。位置を間違えてもJSONとしては解釈できてしまうため、エラーを出さずに黙って無視されます。公式ガイドは、claude --debug で起動してデバッグログの Hook JSON output had unrecognized keys を探せば、無視されたキーが分かると案内しています。
Stop フックが止まらなくなった場合は、ループ保護に当たっています。公式リファレンスのStopによると、ツール呼び出しを挟まずに連続8回ブロックした時点でClaude Codeがフックを上書きしてターンを終わらせます。公式ガイドによると、このカウントは Claude がツールを呼ぶたびにリセットされます。スクリプト側では入力の stop_hook_active を読み、true なら早めに抜けるのが定石です。上限そのものを上げたいときは CLAUDE_CODE_STOP_HOOK_BLOCK_CAP を使う、と公式ガイドに記載があります。
実行の詳細を見たいときはデバッグログです。claude --debug-file /tmp/claude.log のように任意の場所へ書き出し、別のターミナルで tail -f して追います。起動後にログを有効にしたいときは、セッション中に /debug を実行します。成功したフックは会話に何も出ないのが既定なので、動いた証拠はログか成果物で確かめます。
安全に使うための前提とスキル・サブエージェントとの使い分け
フックの安全性は、実行権限とワークスペースの信頼という二つの前提で決まります。コマンドハンドラーは利用者の権限でシェルコマンドを実行するため、設定を配布・受領する側の両方に確認の手順が要ります。
公式リファレンスの免責は率直です。コマンドフックは利用者の全権限でシェルコマンドを実行し、そのアカウントがアクセスできるファイルを変更・削除・参照できる、と書かれています。設定に追加する前にレビューとテストを行ってください(Security considerations)。コマンドは親プロセスの環境を引き継ぐため、スクリプトの中身には認証情報が見える前提で向き合います。
ワークスペースの信頼も押さえておきます。対話セッションでは、自分の ~/.claude/settings.json のものも含めて、フォルダの信頼ダイアログを受け入れるまで設定ファイル由来のフックは保留されます。一方、-p やSDKのセッションではダイアログが出ず、そのフォルダは信頼済みとして扱われます。つまり、自分が書いたのではないリポジトリに対して claude -p を回すと、そのリポジトリの .claude/settings.json に入っているフックが動きます。公式リファレンスは、事前に .claude/ の設定を確認するか、--bare で起動するか、--settings '{"disableAllHooks": true}' でその実行だけフックを切ることを勧めています。
スクリプトを書くときのベストプラクティスも公式に列挙されています。入力を無条件に信頼せず検証する。シェル変数は引用符で囲む。パスに .. が含まれていないか確認する。スクリプトはフルパスで指定する。.env や .git/ のような機微なファイルは避ける。いずれも、フックが自動で走るという性質から来る当然の備えです。CI上での権限の考え方はClaude Code GitHub Actionのセキュリティ解説も合わせて確認してください。
組織で配る場合は管理ポリシー設定が効きます。公式リファレンスによると、管理者が allowManagedHooksOnly を使うと、利用者・プロジェクト・ローカルのフックと、ほとんどのプラグインのフックがブロックされます。HTTPハンドラーについては allowedHttpHookUrls で送信先URLを、httpHookAllowedEnvVars でヘッダーに差し込める環境変数を制限できます。大規模な環境でClaude Codeを走らせる実例としては、アルバータ州政府の大規模コードスキャン事例が参考になります。契約や導入形態の全体像はClaude料金プランの比較を確認してください。
最後に使い分けです。変わらないプロジェクトの決まりごとは CLAUDE.md に書きます。公式リファレンスも、変化しない指示はスクリプトを走らせずに読み込める CLAUDE.md の方が適していると述べています。判断や手順の塊はスキル、独立した文脈で調べ物をさせたいならサブエージェントです。そして「このタイミングで、この処理を、モデルの気分に関係なく走らせたい」という要件だけがフックの担当になります。サブエージェント側のフロントマターにもフックを書けますが、その場合はサブエージェントの実行中だけ有効です。スキルのフロントマターに書いたフックは、呼び出した後のセッション全体で有効なままになり、一度成功したら外したい場合は once: true を付けます。
よくある質問(FAQ)
- Q1. フックはどの設定ファイルに書くのが基本ですか
- チーム共有なら
.claude/settings.json、自分のマシン全体なら~/.claude/settings.json、そのプロジェクトで自分だけ試すなら.claude/settings.local.jsonです。置き場所の一覧は公式リファレンスのHook locationsにあります。迷ったらローカル設定で試し、動くと分かってから共有設定へ移すのが安全です。 - Q2. マッチャーを省略するとどうなりますか
"*"、空文字、省略のいずれでも、そのイベントが起きるたびに発火します。ツールを絞りたいならBashのようにツール名を書き、複数なら|か,で並べます。なお、マッチャーを受け付けないイベントにmatcherを書いた場合は、エラーにならず黙って無視されます。- Q3. ツールの実行を止めるにはどう書きますか
PreToolUseのハンドラーでhookSpecificOutput.permissionDecisionにdenyを返すか、終了コード2で抜けます。PostToolUseはツールが走った後に発火するため、止める用途には使えません。終了コード1では止まらない点にも注意してください。- Q4. フックが登録できているか確かめる方法はありますか
- Claude Codeで
/hooksと入力すると、フックのブラウザが開き、登録したフックがイベント名の下に表示されます。実行の詳細まで追いたいときはclaude --debug-fileでログを出し、終了コードや出力を見ます。 - Q5. 一度実行されたツールの結果を取り消せますか
- 取り消せません。
PostToolUseのupdatedToolOutputで差し替えられるのはClaudeに見える内容だけで、書き込みや実行はすでに終わっています。実行そのものに介入したい場合はPreToolUseを使います。 - Q6. 全部のフックを一時的に止められますか
- 設定ファイルに
"disableAllHooks": trueを書けば止まります。その実行だけ切りたい場合は--settings '{"disableAllHooks": true}'を渡します。ただし管理ポリシー設定で配られたフックは、管理設定側で指定しない限り止まりません。プロジェクトの設定にfalseがあるとユーザー設定のtrueが上書きされる点にも注意してください。個別のフックだけを残したまま無効化する仕組みは用意されていません。 - Q7. プラグインやスキルのフックも同じ書き方ですか
- 形式は同じです。プラグインは
hooks/hooks.json、スキルとサブエージェントはフロントマターに書きます。違いは有効な期間で、サブエージェントのものは実行中だけ、スキルのものは呼び出し以降のセッションで残ります。
まとめ
Claude Codeのフックは、イベント・マッチャー・ハンドラーの三階層をJSONで書くだけの仕組みです。最後に要点を整理します。
- 置き場所で適用範囲が決まる。チーム共有は
.claude/settings.json、自分専用は~/.claude/settings.jsonかローカル設定 - イベント名と設定キーは公式ページに載っているものだけを使う。綴りが違うとそのエントリだけ静かに飛ばされる
- マッチャーは、英数字と限られた記号だけなら完全一致、それ以外の文字が混ざると正規表現になる
- 止めたいなら終了コード2か
permissionDecision: "deny"。終了コード1では止まらない PreToolUseの判断はhookSpecificOutputの中、PostToolUseやStopはトップレベルのdecision- コマンドフックは利用者の権限で動く。配布物のフックは中身を読んでから受け入れる
まずは編集後に整形を走らせる一本を入れて、/hooks で登録を確かめるところから始めてみてください。仕様の細部は版によって更新されるため、判断に迷ったら公式のHooksリファレンスとガイドを突き合わせるのが近道です。Claude Codeの基本操作を復習したい方はClaude Codeの使い方、役割を分けて作業させたい方はサブエージェントの使い方へ進むと、自動化の全体像がつながります。
あわせて読みたい・姉妹メディア
