Claude Code Hooks(フック)は、Claude Code がファイルを編集する直前・直後、応答を終えたとき、セッションを始めたときなど、決まった時点で登録済みの処理を自動で走らせる仕組みです。CLAUDE.md に書いたルールは Claude への依頼にとどまりますが、フックは設定ファイルの記述なので、条件に合えば Claude の判断を経ずに必ず起動します。この記事では、CLAUDE.md・Skills との使い分け、全33イベント、settings.json の書き方、終了コードとJSONでの判定の返し方、失敗時に操作を止める onFailure、動かないときの切り分けまでを、v2.1.303(2026年10月18日UTC公開)時点の公式リファレンスに沿って整理します。
まとめ
フックは settings.json の hooks キーに「イベント名 > マッチャーグループ > ハンドラ」の3階層で書きます。イベントは33種類ありますが、最初に入れるなら PostToolUse(編集後の整形)・PreToolUse(危険な操作の拒否)・Notification(入力待ちの通知)・Stop(応答完了の通知)の4つで足ります。実行する処理は command・http・mcp_tool・prompt・agent の5種類です。
判定は終了コードかJSONで返します。PreToolUse では、終了コード2はJSONで allow を返しても覆らないブロックです。一方、多くのイベントでは、スクリプトが見つからない・タイムアウトしたといった失敗が既定では操作を止めません。ただし、PreModelSwitch のタイムアウトなどには例外があります。ポリシー用のフックには v2.1.295 で加わった "onFailure": "block" を付けてください。一致したフックはすべて並列に走り順序は決まらないため、実行順に頼る設計は成り立ちません。古い解説に見られる Pre-Command というイベント名や priority での順序指定は、公式仕様に存在しません(対照表)。
Claude Code Hooksの仕組みとCLAUDE.md・Skillsとの違い
公式ガイドは、フックを「Claude Code のライフサイクルの特定の地点で実行されるユーザー定義のシェルコマンド」と説明し、LLM が実行を選ぶのに頼らず特定の動作が必ず起きる「決定論的な制御」だと位置づけています(Hooks guide)。ターミナル、IDE拡張、デスクトップアプリで同じイベントが発火します。ブラウザ版のクラウドセッションだけは手元の ~/.claude/settings.json を読みません。
イベント・matcher・ハンドラの3点で動く流れ
動作は3段階です。イベント(例: PreToolUse)が起き、マッチャー(例: ツール名が Bash)に一致すると、Claude Code はイベントの情報をJSONでハンドラに渡します。command ハンドラなら標準入力、http ハンドラならPOSTの本文で届きます。ハンドラは終了コードや標準出力のJSONで「止める」「通す」「Claude に文脈を足す」などの結果を返します。何も返さなければ判定なしとして扱われ、処理は通常どおり進みます。
CLAUDE.md・Skills・MCPとの使い分け
公式の機能概要は、拡張手段を「何がきっかけで動くか」で分けています。フックだけが、Claude の判断を通らずにイベントで起動します。
| 機能 | 動くきっかけ | 確実に起きるか | コンテキスト消費 | 向く用途 |
|---|---|---|---|---|
| CLAUDE.md | 毎回の会話で読み込み | 指示として読むだけ | 常に消費 | 規約・前提の共有 |
| Skill | /名前 か説明文との一致 | Claudeの判断次第 | 本文は使用時 | 手順書・参照資料 |
| MCP | Claudeがツールを選ぶ | Claudeの判断次第 | ツール定義を読む | 外部サービス接続 |
| フック | ライフサイクルのイベント | 条件に合えば必ず起動 | 出力を返したときだけ | 整形・拒否・通知・記録 |
判断の基準は公式が明言しています。CLAUDE.md や Skill に書いた「.env を編集しない」は「依頼であって保証ではなく」、編集をブロックする PreToolUse フックが強制だ、という整理です。毎回同じ手順で Claude が考える必要のない処理(保存時の整形、rm -rf の拒否、終了時の通知)はフックへ移します。状況に応じて Claude が手順を選ぶべきもの(リリース手順、設計規約)は Skill か CLAUDE.md に残します。リンターの実行をフックで確実に起こし、出たエラーの直し方を Skill に書く、という組み合わせが実務の形です。Skill の書き方はClaude Skillsとは?SKILL.mdの書き方とclaude.ai・Claude Code・APIでの使い方【2026年10月】にまとめています。
フックは単体でも、プラグインに hooks/hooks.json として同梱しても配れます(Claude Codeプラグインの作り方と社内配布|marketplace.json・管理設定【2026年9月版】)。プラグイン内の TypeScript 関数でツール呼び出しや画面を書き換える Mods は別の仕組みで、違いはClaude Code Modsとは|Hooksとの違い・作り方・組織管理の実装で扱っています。
React HooksやGitフックなど一般の「フック」との違い
「hooks」は Claude Code 固有の語ではありません。英語の hook は「引っかける鉤」で、プログラミングでは「決まった地点に自分の処理を差し込む口」を指します。Git のコミット前後に走るスクリプト、Zsh の precmd・preexec、React の useState もフックと呼ばれますが、共通するのはこの構造だけで、書き方も設定場所も別物です。Anthropic の日本語ドキュメントは「フック」と表記しており、「フックス」というカナは使っていません。React のフックはReactフック(Hooks)とは?全18種の一覧とReact Compiler時代の使い分け【2026年8月版】、Zsh のフックはadd-zsh-hookでZsh Hookを登録する方法|precmd・preexecなど発動タイミングと実例が扱います。
ハンドラ5種類の使い分けと既定タイムアウト
決まったルールで判定できるなら command、文脈を読んだ判断が要るなら prompt か agent を選びます。追加された版はCHANGELOGの記載です。
| type | 実行するもの | 既定タイムアウト | 追加された版 |
|---|---|---|---|
| command | シェルコマンド | 600秒 | 1.0.38 |
| http | URLへのPOST | 600秒 | 2.1.63 |
| mcp_tool | 接続済みMCPサーバーのツール | 600秒 | 2.1.118 |
| prompt | モデルによる1回の判定 | 30秒 | 2.0.30(当初はStop用) |
| agent | ツールを使えるサブエージェント | 60秒 | 実験的機能 |
既定値はイベントによって下がります。UserPromptSubmit・PreModelSwitch・PostModelSwitch では上3種が30秒、MessageDisplay は10秒です。SessionEnd は全ハンドラ合計で1.5秒しかなく、長い timeout を書いたときだけ最大60秒まで延びます。終了時に重い後処理を積むと打ち切られます。
agent は公式が「実験的で変更される可能性がある」と明記しており、PermissionRequest では使えません(設定しても飛ばされます)。v2.1.302 からは同じイベントの prompt・agent フックが同時に12本までしか走りません。http でブロックするには、ステータスコードではなく2xxの応答本文でJSONの判定を返します。
フックイベント全33種の一覧と発火タイミング
公式リファレンスの一覧は33種類です。日本語の解説記事には14種類や30種類のまま止まっているものがあるので、目的のタイミングが見つからなければこの表で探してください。
| イベント | 発火タイミング | matcherの対象 |
|---|---|---|
| SessionStart | セッション開始・再開時 | startup / resume / clear / compact / fork |
| Setup | –init-only などの初期化起動時 | init / maintenance |
| UserPromptSubmit | プロンプト送信直後 | なし |
| UserPromptExpansion | コマンドがプロンプトへ展開される前 | コマンド名 |
| PreToolUse | ツール実行の直前 | ツール名 |
| PermissionRequest | 権限確認が要るとき | ツール名 |
| PermissionDenied | auto modeが拒否したとき | ツール名 |
| PostToolUse | ツール実行の成功後 | ツール名 |
| PostToolUseFailure | ツール実行の失敗後 | ツール名 |
| PostToolBatch | 並列ツール群がそろった後 | なし |
| Notification | 通知の送出時 | 通知種別 |
| MessageDisplay | 応答テキストの表示中 | なし |
| SubagentStart | サブエージェント起動時 | エージェント種別 |
| SubagentStop | サブエージェント終了時 | エージェント種別 |
| TaskCreated | タスク作成時 | なし |
| TaskCompleted | タスク完了の記録時 | なし |
| Stop | 応答完了時 | なし |
| StopFailure | APIエラーでターン終了時 | エラー種別 |
| TeammateIdle | チームメイトが待機に入る前 | なし |
| InstructionsLoaded | CLAUDE.md等の読み込み時 | 読み込み理由 |
| ConfigChange | 設定ファイルの変更時 | 設定の出所 |
| CwdChanged | 作業ディレクトリの変更時 | なし |
| DirectoryAdded | /add-dir での追加時 | 追加方法 |
| FileChanged | 監視対象ファイルの変更時 | ファイル名 |
| WorktreeCreate | ワークツリー作成時 | なし |
| WorktreeRemove | WorktreeCreateフック由来のワークツリー削除時 | なし |
| PreCompact | コンテキスト圧縮の前 | manual / auto |
| PostCompact | コンテキスト圧縮の後 | manual / auto |
| PreModelSwitch | モデル切替の適用前 | 切替先モデル名 |
| PostModelSwitch | モデル切替の完了後 | 切替先モデル名 |
| Elicitation | MCPサーバーが入力を求めたとき | サーバー名 |
| ElicitationResult | 入力への応答を返す前 | サーバー名 |
| SessionEnd | セッション終了時 | 終了理由 |
発火の頻度は3系統です。セッションに1回の SessionStart・SessionEnd、ターンごとの UserPromptSubmit・Stop・StopFailure、ツール呼び出しのたびの PreToolUse・PostToolUse。ツール系は1ターンで何十回も走るので、重い処理を置くと体感速度に直結します。重い処理は command フックに "async": true を付けてバックグラウンドへ逃がせますが、-p の非対話実行では終了時に打ち切られ cancelled になります。
拒否に使うPreToolUseとPermissionRequestの違い
拒否に使うイベントは PreToolUse と PermissionRequest ですが、守備範囲が違います。PreToolUse は EndConversation を除くツール呼び出しの前に、権限確認の有無にかかわらず走り、PermissionRequest は権限確認が必要になったときに走り、確認画面を出せず通常なら自動拒否される呼び出しでも発火します。許可済みの操作まで止めたいなら PreToolUse を選びます。PostToolUse で危険な操作を止める設計は成り立ちません。そこはツールが実行済みの地点だからです。
StopとSubagentStop、StopFailureの違い
Stop はメインのエージェントが応答を終えたときで、ユーザーの割り込みでは発火しません。APIエラーでターンが終わると StopFailure が発火し、rate_limit・overloaded・billing_error などのエラー種別で絞り込めます。サブエージェントの完了は SubagentStop です。Stop の入力には background_tasks と session_crons が入るので、完了通知のフックはこれを見て「バックグラウンド処理の完了待ちで止まっただけ」の場合を除外します。応答本文は last_assistant_message から取ります。トランスクリプトファイルは非同期に書かれるため、最終メッセージを含む保証がありません。
settings.jsonにフックを設定する手順
フックはJSONの設定ファイルに書きます。外側の hooks がイベント名をキーに取り、配列の各要素がマッチャーグループ、その中の hooks 配列がハンドラです。同じ名前が2回出て紛らわしいのですが、実際に走る処理を持つのは内側だけです。編集後に整形スクリプトを走らせる最小構成は次のとおりです。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh",
"args": []
}
]
}
]
}
}
置き場所7か所と共有範囲
適用範囲と共有の可否は置き場所で決まります。チームに配るならプロジェクト設定かプラグイン、自分専用ならユーザー設定かローカル設定です。
| 置き場所 | 適用範囲 | 共有 |
|---|---|---|
| ~/.claude/settings.json | 全プロジェクト | 不可 |
| .claude/settings.json | そのプロジェクト | 可(コミット) |
| .claude/settings.local.json | そのプロジェクト | 不可 |
| 管理ポリシー設定 | 組織全体 | 可(管理者) |
| プラグインの hooks/hooks.json | 有効化中のみ | 可(同梱) |
| Skillのフロントマター | 呼び出し後のセッション | 可 |
| サブエージェントのフロントマター | そのエージェント実行中 | 可 |
各階層のフックは置き換えではなく合流します。同じハンドラを複数の設定ファイルに書いても実行は1回ですが、プラグインやSkill由来の同一ハンドラは別扱いで重複して走ります。設定ファイル由来のフックはサブエージェントの中でも発火し、入力の agent_id・agent_type でどのエージェントかを見分けられます(Claude Codeのサブエージェントとは?作り方・設定項目・使い方【v2.1.287対応】)。組織で配る場合は、管理ポリシー設定の allowManagedHooksOnly でユーザー・プロジェクト・ローカル設定と通常のプラグイン由来のフックを止められます。ただし、管理設定の enabledPlugins で強制有効化されたプラグインのフックは例外です。
exec形式とshell形式、パスプレースホルダの指定
args の有無で起動方法が変わります。args があれば exec 形式で、command を実行ファイルとして直接起動し、各要素がそのまま1引数になります。シェルを通らないので、パスに空白が入ってもクォートが要りません。args が無ければ shell 形式で、macOS・Linux では sh -c に渡るためパイプや && が使えます。
スクリプトのパスを ${CLAUDE_PROJECT_DIR} や ${CLAUDE_PLUGIN_ROOT} で書くなら "args": [] を付けて exec 形式にします。パイプやリダイレクトが要るときだけ shell 形式にし、プレースホルダをダブルクォートで囲みます。Windows では npx などの .cmd を exec 形式で起動できないため、node に実スクリプトのパスを渡します。ワークツリーに入った後も ${CLAUDE_PROJECT_DIR} はセッション開始時のプロジェクトルートを指したままです。Claude が今いるディレクトリは入力JSONの cwd で読みます。
目的別の設定例:通知・自動整形・保護・再注入
公式ガイドが挙げる使い道を、使うイベントごとにまとめます。
| 目的 | イベント | 処理の中身 |
|---|---|---|
| 入力待ち・許可待ちの通知 | Notification | デスクトップ通知・Slack投稿 |
| 編集後の自動整形 | PostToolUse(Edit|Write) | Prettierなどを実行 |
| 保護ファイルの編集禁止 | PreToolUse(Edit|Write) | パスを見て終了コード2 |
| 圧縮後の前提の再注入 | SessionStart(compact) | 標準出力に前提を書く |
| 設定変更の監査 | ConfigChange | 変更をログに追記 |
| 環境変数の再読み込み | CwdChanged・FileChanged | direnvなどを再実行 |
| 終了前のテスト通過確認 | Stop | テスト失敗なら継続させる |
入力待ち・作業完了のデスクトップ通知設定
公式ガイドの最初の例は、~/.claude/settings.json の Notification に macOS の osascript を登録するものです。マッチャーに permission_prompt を書けば、許可待ちが約6秒続いたときだけに絞れます。
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
通知が出ないときは、macOSの「システム設定 > 通知」でスクリプトエディタの通知が許可されているかを確かめます。Slackへ送るなら、command の中で Incoming Webhook のURLに curl でPOSTします(Slack Incoming Webhookの実装手順:URL発行・Block Kit整形・SDK送信と429対策)。
作業完了の通知は Stop に置きます。フックには制御端末が無いので /dev/tty にエスケープシーケンスを書く方法は使えません。JSONの terminalSequence に載せて Claude Code に出力させます。許可されるのは OSC の 0・1・2・9・99・777 と BEL だけで、それ以外を含めるとフィールドごと無視されます。
#!/bin/bash
SEQ=$(printf '\033]9;Claude Code の作業が完了しました\007')
jq -n --arg seq "$SEQ" '{terminalSequence: $seq}'
bash 3.2.57 と jq 1.7.1 で実行すると、terminalSequence だけを持つJSONが出力され、値の先頭がエスケープ文字、末尾がBELになりました。制御バイトは printf で作り、JSON化は jq に任せます。文字列連結で組むと引用符の扱いを誤ってJSONが壊れます。この通知は対話セッションでだけ出力され、-p とエージェントSDKでは無視されます。
PostToolUseとPrettierによる編集後の自動整形
PostToolUse に Edit|Write を当て、編集されたファイルのパスを jq で取り出して Prettier に渡します。スクリプトファイルを作らずに済むので、最初の1本に向いています。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "file=$(jq -er '.tool_input.file_path') && npx prettier --write \"$file\""
}
]
}
]
}
}
PostToolUse の標準出力は、終了コード0のままではデバッグログに行くだけで Claude には届きません。エラーを伝えるなら標準エラーに書いて終了コード2で抜けるか、JSONの additionalContext で渡します。
PreToolUseによる保護ファイルの編集拒否
PreToolUse に同じマッチャーを当て、パスが保護対象に当たれば終了コード2で止めます。標準エラーに書いた理由が Claude に届くので、Claude は別の手段を探し直せます。
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
FILE_PATH="${FILE_PATH//\\//}"
PROTECTED=(".env" "package-lock.json" ".git/")
for p in "${PROTECTED[@]}"; do
if [[ "$FILE_PATH" == *"$p"* ]]; then
echo "Blocked: $FILE_PATH matches protected pattern '$p'" >&2
exit 2
fi
done
exit 0
4行目は Windows の円記号区切りをスラッシュにそろえる行です。bash 3.2.57 と jq 1.7.1 で {"tool_name":"Write","tool_input":{"file_path":"/proj/.env"}} を流すと終了コード2、C:\proj\.git\config も C:/proj/.git/config に直ったうえで終了コード2、/proj/src/a.ts は出力なしで終了コード0でした。部分一致なので .env.example も止まります。また、Edit・Write だけが対象で、Bash や MCP ツール経由の書き込みは防げません。標準入力のJSONを読めれば言語は問わないので、jq が無い環境では Python の json.load(sys.stdin) で同じ判定を書けます。
SessionStartによるコンテキスト圧縮後の前提再注入
会話が長くなるとコンテキストが圧縮され、細かな前提が落ちます。SessionStart を compact のマッチャーで登録すると、圧縮のたびに標準出力の文字列が Claude の文脈へ足されます。SessionStart は、素の標準出力が Claude に届く数少ないイベントの1つです。
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Reminder: use Bun, not npm. Run bun test before committing.'; git log --oneline -5"
}
]
}
]
}
}
毎回のセッション開始で同じことを伝えたいだけなら、公式の案内どおり CLAUDE.md で足ります。
matcherとifによる発火範囲の絞り込み
matcher はイベント単位のフィルタで、省略するか "*" か空文字にすると全発火で走ります。注意が要るのは、書いた文字によって評価方法が切り替わる点です。英数字・_・-・空白・,・| だけなら完全一致(| か , 区切りの複数指定可)、それ以外の文字が1つでも入るとアンカーなしのJavaScript正規表現になります。Edit.* は正規表現扱いで NotebookEdit にも一致するので、1つのツールだけに当てたいなら ^Edit$ と書きます。FileChanged と StopFailure だけは完全一致の文字が英数字・_・| に限られ、区切りは | しか使えません。マッチャーは大文字小文字を区別します。ただし FileChanged の監視対象リストは別で、matcher を | で分割した各値を文字どおりのファイル名として登録します。正規表現で監視対象を増やすことはできません。
MCPツールへの一致とifによる引数レベルの絞り込み
MCPサーバーのツールは mcp__サーバー名__ツール名 という名前で現れます。あるサーバーの全ツールに当てるには末尾の .* が必須で、mcp__memory とだけ書くと完全一致になり、どのツールにも一致しません。mcp__memory__.* と書きます。
ツール名で絞りきれないときはハンドラ側の if を使います。権限ルールと同じ構文で、"Bash(git *)" なら Bash のサブコマンドのどれかが git * に当たったとき、"Edit(*.ts)" なら TypeScript の編集時だけプロセスを起動します。
{
"type": "command",
"if": "Bash(rm *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
"args": []
}
if には1つのルールしか書けず、&& も || も配列も使えません。条件が複数あればハンドラを分けます。評価されるのはツール系イベント(PreToolUse・PostToolUse・PostToolUseFailure・PermissionRequest・PermissionDenied)だけです。v2.1.214 以降、"Edit(src/**)" は作業ディレクトリ直下の src だけに当たり、どの階層の src にも当てたいなら "Edit(**/src/**)" と書きます。
終了コードとJSON出力による判定の返し方
返し方は、終了コードだけで合図するか、終了コード0のままJSONを標準出力に書くかの2系統です。
終了コード0・2・それ以外の扱い
結果は終了コードと標準出力の組み合わせで決まります。PreToolUse の場合の公式の表を要約します。
| 標準出力 | 終了コード0 | 終了コード2 | それ以外 |
|---|---|---|---|
| 検証を通るJSON | JSONの判定を適用 | ブロック(JSONで覆せない) | JSONの判定を適用 |
| 壊れたJSON | 非ブロックのエラー | ブロック | 非ブロックのエラー |
| テキストか空 | 判定なしで続行 | ブロック(標準エラーが理由) | 非ブロックのエラー |
要点は3つです。終了コード2は、JSONで permissionDecision: "allow" を返していてもブロックになります。Unix で一般的な終了コード1は、JSONを返さなければ「非ブロックのエラー」で操作は通ります(ポリシー用なら2を使う)。そして終了コード0の無出力は「許可した」ではなく「判定を出していない」という意味で、ツール呼び出しは通常の権限フローへ進みます。標準出力は1つのJSONオブジェクトだけを書いたときにJSONとして読まれ、前後に別の文字列があるとテキスト扱いになります。additionalContext などの文字列は1万文字が上限で、超えるとファイルに退避されます。
PreToolUseの4値とupdatedInput
PreToolUse の判定は hookSpecificOutput の中に書き、allow・deny・ask・defer の4値を返せます。複数のフックが別々の判定を返したときは deny > defer > ask > allow の順に優先されます。defer は -p の非対話実行でだけ効き、対話セッションでは警告を出して無視されます。また、同時に複数のツールを呼ぶ場合も defer は無視され、通常の権限フローへ進みます。次のスクリプトは rm -rf という文字列を含む Bash コマンドを拒否する簡易例です。rm -fr やオプションを分けた記述は検出せず、引用文字列中の一致も拒否します。
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
case "$COMMAND" in
*"rm -rf"*)
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "rm -rf を含むため hook でブロックしました"
}
}'
;;
*) exit 0 ;;
esac
bash 3.2.57 と jq 1.7.1 で {"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/build"}} を流すと、permissionDecision が deny のJSONが出て終了コード0、npm test を流すと出力なしで終了コード0でした。
updatedInput を返すと、ツールの引数を実行前に書き換えられます。オブジェクト全体を差し替える仕様なので、変えないフィールドも含めて返します。v2.1.300 からは Claude 自身の入力と同じく検証され、未知のキーは捨てられ、ツールが受け付けない入力は呼び出しごと拒否されます。複数のフックが同じ入力を書き換えると最後に終わったものが勝ち、並列実行のため順序は決まりません。入力を書き換えるフックは1つに絞ります。
Stopフックの8回上限とstop_hook_active
Stop と SubagentStop は decision: "block" で停止を止められます。「テストを通すまで終わらせない」が書ける反面、条件が満たされないと終わりません。Claude Code は連続8回の継続で次のブロックを無視してターンを終えます(Claude がツールを呼ぶたびに回数はリセット)。上限は環境変数 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP で変えられます。ループを自分で止めるには、入力の stop_hook_active が true のとき素通しします。
#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
# この下に本来の判定を書く(例: テストが失敗していたら exit 2)
指示を渡したいだけならブロックではなく additionalContext を返します。会話は同じ上限のもとで続き、トランスクリプトにはエラーではなく Stop hook feedback として表示されます。
フックが失敗したときに操作を止める設定(onFailure)
ポリシー用のフックで最も見落とされるのが、フック自体が失敗したときの扱いです。多くのイベントでは、スクリプトが起動しない、タイムアウトした、JSONが壊れている、終了コードが0と2以外でJSONを返さない、のどれも既定では「非ブロックのエラー」となり、操作はそのまま進みます(終了コードが0と2以外でも、有効なJSONを返せばその判定が適用されます)。例外は、タイムアウトでモデル切替を止める PreModelSwitch と、Git Bash の無い Windows で shell 形式の PreToolUse・PermissionRequest フックが失敗した場合です。settings.json のパスを1文字打ち間違えただけで、拒否するはずのフックが何も止めない状態になります。PreToolUse の command・http・mcp_tool フックがタイムアウトした場合も同じで、公式は「止まったフックを関門として当てにしない」よう書いています。
v2.1.295 で、command と http のハンドラに "onFailure": "block" が加わりました。失敗したときに、そのイベントで終了コード2を返したのと同じ扱いになります(PermissionRequest では要求の拒否)。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check-command.js"],
"onFailure": "block"
}
]
}
]
}
}
Claude Code 2.1.302 の claude -p(--model haiku --allowedTools Bash)で、存在しないスクリプトを指す PreToolUse フックを付け、ファイルを書く Bash コマンドを1回実行させて比べました。onFailure を付けない設定ではコマンドがそのまま実行されてファイルが作られ、Claude の報告にもフックのエラーは出ませんでした。"block" を付けた設定ではファイルは作られず、Claude は PreToolUse:Bash hook error: [node /tmp/hooktest9864/missing-check.js]: failed; blocking because onFailure is "block" とNode.jsの MODULE_NOT_FOUND を受け取りました。
判断は単純です。拒否を担うフックには "onFailure": "block" を付けます。整形や通知のように、失敗しても作業を止めたくないフックには付けません。編集後の PostToolUse に付けても、実行済みの編集は止められません。失敗はそのイベントの終了コード2と同じ扱いになり、Claude にエラーが伝わります。Stop・SubagentStop・TaskCompleted・TeammateIdle では効きません。これらで終了コード2は「作業を続けさせる」意味になり、動かないフックを Claude が直せないためです。なお、終了コード1でJSONの allow を返したフックも "block" のもとでは失敗扱いになるので、JSONで判定を返すなら終了コード0で抜けます。
フックが動かないときの切り分け手順
設定したのに何も起きない場合は、登録・出力・実行の順に確かめます。
/hooksメニューとVS CodeのHooksダイアログによる登録確認
セッション内で /hooks を実行すると、設定済みのフックがイベントごとに1つの一覧で出ます(v2.1.286 以降)。各フックには出どころ(ユーザー設定・プロジェクト設定・プラグインなど)が付き、選ぶと実行内容と定義場所が見られます。このメニューは閲覧専用で、追加も削除もできません。VS Code 拡張では v2.1.269 で Hooks ダイアログが加わり、ユーザー・プロジェクト・ローカル設定のフックを追加・編集・削除できます(管理設定・プラグイン由来は読み取り専用)。一覧に出ないならJSONの構文エラー(末尾カンマ、コメント)を疑います。設定ファイルの変更は通常ファイル監視で自動反映され、数秒待っても出ないときだけセッションを再起動します。
シェルプロファイルの出力混入によるJSON判定の無効化
フックは動いていて正しいJSONも出しているのに判定が効かない。この症状で公式ガイドが節を立てているのが、シェルプロファイルの出力の混入です。プロファイルに無条件の echo があると、その出力がJSONの前に付き、全体が { で始まらなくなって判定ごとテキスト扱いになります。対話シェルのときだけ出力するように直します。
if [[ $- == *i* ]]; then
echo "Shell ready"
fi
ほかの頻出原因は、相対パスによる command not found、chmod +x の付け忘れ、jq の未導入、ツール名の大文字小文字の違いです。トランスクリプトに Failed with non-blocking status code: /bin/sh: ...: No such file or directory が出ていたら、フックは一度も走っていません。
デバッグログの読み方
claude --debug-file /tmp/claude.log で起動すると、フックの一致・終了コード・出力が既知のパスに残ります。フラグ無しで始めたら、セッション中に /debug で有効にできます。v2.1.296 以降は、ツール系・UserPromptSubmit・SessionStart・Stop などの command フックが終わるたびに Hooks: PostToolUse:Write [.claude/hooks/log-write.sh] finished with status 0 (31ms) の形で所要時間まで記録されるので、遅いフックを特定できます。マッチャーの一致判定まで見るなら CLAUDE_CODE_DEBUG_LOG_LEVEL=verbose を設定します。セッションを起動しなくても、サンプルJSONをスクリプトにパイプすれば単体で確かめられます。
権限モードとの関係と、フックに任せてはいけない境界
PreToolUse は権限モードの判定より前に、すべてのモードで発火します。bypassPermissions でも --dangerously-skip-permissions でも deny を返せばツールは止まります。緩める方向には上限があります。フックの allow は通常の権限確認を省略できますが、設定の deny ルールは覆らず、requiresUserInteraction 付きのMCPツールの確認も省けません。各モードの違いはClaude Codeのパーミッションモードの違いと設定方法|auto・acceptEdits・bypassPermissionsの使い分けにまとめています。
フックをセキュリティ境界そのものにはしないでください。if の絞り込みはベストエフォートで、Bash の入力が実際にどのコマンドを走らせるか判定できないとき、Claude Code はパターンに関係なくフックを起動します。公式は、確実な許可・拒否には権限システムを使うよう指示しています。フックは監査・整形・通知・追加チェックの層に置き、絶対に通したくない操作は権限ルールの deny で止めます。
実務で最も危ないのはワークスペース信頼の非対称です。対話セッションでは、自分の ~/.claude/settings.json のフックすら信頼ダイアログを受け入れるまで保留されます。ところが -p と SDK のセッションではダイアログが出ず、フォルダは信頼済みとして扱われます。一度も信頼していないリポジトリにコミットされた .claude/settings.json のフックが、自分の権限で走るということです。他人のリポジトリに claude -p を回すなら、事前に .claude/ 配下を読むか、--bare で起動するか、--settings '{"disableAllHooks": true}' でその実行だけフックを止めます。管理ポリシー設定のフックだけは、管理レベルで止めない限り動きます。
他のAIコーディングエージェントのフックとの比較
「AIのフック」「LLMのフック」と呼ばれる仕組みは、Claude Code が 1.0.38(npm公開は2025年6月30日UTC)で公開した後、主要なコーディングエージェントに相次いで入りました。設定ファイルの場所とイベント名は違いますが、ツール実行前のイベントで終了コード2か拒否のJSONを返すと止まる、という基本の形は共通です。
| 製品 | 設定ファイル | ツール実行前のイベント |
|---|---|---|
| Claude Code | .claude/settings.json ほか | PreToolUse |
| Cursor | .cursor/hooks.json | preToolUse・beforeShellExecution |
| GitHub Copilot CLI | .github/hooks/*.json | preToolUse |
| Gemini CLI | .gemini/settings.json | BeforeTool |
| OpenAI Codex | .codex/hooks.json か config.toml | PreToolUse |
複数のエージェントを併用するチームでは、判定ロジックを1本のスクリプトにまとめ、各製品の設定から呼ぶ形にすると管理が楽になります。ただし入力JSONの項目名は製品ごとに違うので、共通化の前に各リファレンス(Cursor・GitHub Copilot・Gemini CLI・Codex)で確かめます。Cline のフックはClineの設定ファイルとインストール手順|.clineの中身・MCP設定・Hooksの置き場所(v4.1対応)、エージェントを制御する部品としてのフックの位置づけはAIハーネスとは?LLMエージェントの制御基盤を構成する6部品とSDK別の実装で扱っています。社内の開発フローにこうしたガードレールを組み込んだエージェントを設計する場合は、AIエージェント開発の支援内容も参考にしてください。
フック設定の誤記と現行仕様の対照
フックは 1.0.38 の CHANGELOG に「Released hooks.」とあるのが始まりで、1.0.54 で UserPromptSubmit、2.0.30 で prompt ハンドラ、2.1.63 で http、2.1.118 で mcp_tool、2.1.295 で onFailure が加わりました。初期の日本語解説には公式仕様に無い記述が混ざっており、本記事の旧版もその1つでした。
| よくある記述 | 現行仕様 |
|---|---|
| Pre-Command / Post-Command フック | 該当名なし。PreToolUse / PostToolUse |
| hooks.yaml に書く | settings.json などにJSONで書く |
| カスタムイベントを定義できる | 不可。33種の定義済みイベントのみ |
| priority で実行順を制御 | 該当キーなし。全て並列・順序は非決定 |
| trigger / retries で再試行 | 該当キーなし |
| /hooks で追加・編集する | 閲覧専用(VS Codeはダイアログで編集可) |
| claude-code hook register / list | 該当CLIなし |
| enabled: false で個別に無効化 | 不可(disableAllHooks で全体のみ) |
| 失敗したら操作も止まる | 既定は続行。止めるなら onFailure |
| IAMロールで権限を限定 | 実行ユーザーの全権限で動作 |
影響が大きいのは priority です。順序が要るなら、1つのフックスクリプトの中で順に実行します。使っている版の確かめ方はClaude Codeの最新バージョンは2.1系|確認方法・アップデート手順とstable版の違いにまとめています。
よくある質問
Claude Code Hooksとは何ですか?
Claude Code のライフサイクルの決まった時点(ツール実行の前後、応答完了、セッション開始など)で自動実行される、ユーザー定義の処理です。1.0.38 で追加されました。プロンプトでの依頼と違い、Claude の判断を経ずに条件どおり起動します。設定は settings.json の hooks キーに書きます。
hooksの意味と読み方は何ですか?
読み方は「フック」です。英語の hook(鉤)から来た語で、プログラムの決まった地点に自分の処理を差し込む口を指します。Anthropic の日本語ドキュメントも「フック」「コマンド フック」と表記しています。React Hooks や Git フックも同じ語ですが、仕組みは別物です。
HooksとCLAUDE.mdはどちらを使えばよいですか?
必ず守らせたいルールはフック、Claude に知っておいてほしい前提や判断の材料は CLAUDE.md です。公式は、CLAUDE.md の「.env を編集しない」は依頼であって保証ではないとし、強制には PreToolUse フックを使うよう書いています。
フックの処理が失敗したらClaude Codeの操作は止まりますか?
多くのイベントでは、起動失敗やタイムアウトだけでは操作は止まりません。ただし、PreModelSwitch のタイムアウトや、Git Bash の無い Windows での一部のフックではブロックされます。また、終了コードが0と2以外でも、有効なJSONを返した場合はその判定が適用されます。止めたいフックには v2.1.295 以降の "onFailure": "block" を付けます。
Hooksを使うと料金やトークンは増えますか?
command フックは Claude Code の外で動くので、出力を返さない限りコンテキストを消費しません。additionalContext などで文字列を返すとその分が文脈に入ります。prompt と agent のハンドラはモデルを呼ぶため、その分の利用量がかかります。