2026年9月3日時点の公式リファレンスで、Claude Code のフックイベントは 33種類、ハンドラーは 5種類(command / http / mcp_tool / prompt / agent)ある。2025年6月30日の v1.0.38 で「hooks」が最初にリリースされた頃の PreToolUse・PostToolUse・Stop・Notification だけの世界とは、設定の書き方も止め方も変わっている。
実務で事故になりやすいのは3点だ。(1) matcher は文字の種類によって「完全一致」と「正規表現」に分岐する、(2) 止められるのは exit 2 だけで exit 1 は素通りする、(3) フックに渡るのは stdin の JSON で、ツール名や編集ファイルのパスを持つ専用の環境変数は存在しない。この3つを外すと、書いたつもりのガードが黙って無効になる。
以下、公式の Hooks reference と changelog(最新は 2.1.259 / 2026年9月2日)に1件ずつ突き合わせて、コピペで動く設定と、動かないときの切り分け手順までをまとめる。
5分で入る「監査ログ」フック(2026年9月版のコピペ設定)
理屈より先に1本動かすのが早い。Claude がどのファイルをいつ書き換えたかを記録する PostToolUse フックから入る。チームで Claude Code を使うときの透明性確保にそのまま効く。
// ~/.claude/settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '\"\\(now|todate) \\(.tool_name) \\(.tool_input.file_path)\"' >> ~/claude-audit.log"
}
]
}
]
}
}
動作環境: Claude Code v2.1 系(本記事は 2.1.259 時点の公式ドキュメントに準拠)、macOS / Linux、jq が PATH にあること。
ポイントは jq に渡している入力だ。フックは stdin に JSON を受け取る。tool_name、tool_input、tool_use_id、session_id、cwd、permission_mode などが1つのオブジェクトで入ってくるので、そこから必要なフィールドを抜く。
ここで注意したいのが環境変数だ。$CLAUDE_TOOL_NAME や $CLAUDE_TOOL_INPUT_FILE_PATH のような「ツール情報を持つ環境変数」は公式リファレンスに存在しない(本記事の旧版はこれを掲載していたので訂正する)。公式が名前を挙げている環境変数は、スクリプトのパス解決に使う CLAUDE_PROJECT_DIR / CLAUDE_PLUGIN_ROOT / CLAUDE_PLUGIN_DATA、SessionStart 系で使う CLAUDE_ENV_FILE、状態を示す CLAUDE_CODE_REMOTE や CLAUDE_EFFORT などで、モデル名を取る $CLAUDE_MODEL すら「存在しない」と明記されている。ツールの中身は必ず stdin から読むこと。
Hooksの全体像|33イベント・5ハンドラー(2026年9月時点)
公式リファレンスは、イベントの発火頻度を3つの周期で整理している。
- セッションに1回:
SessionStart、SessionEnd - ターンに1回:
UserPromptSubmit、Stop、StopFailure - エージェントループの中のツール呼び出しごと:
PreToolUse、PostToolUse(EndConversationの呼び出しだけは両方スキップ)
実務でまず押さえるのはこの範囲だ。
| イベント | 発火タイミング | exit 2 で止められるか | 主な用途 |
|---|---|---|---|
PreToolUse |
ツール実行の直前 | 止まる(ツール呼び出しをブロック) | ポリシー適用・入力の書き換え |
PostToolUse |
ツール成功の直後 | 止まらない(stderr が Claude に見える) | lint・監査ログ・出力の差し替え |
PostToolUseFailure |
ツール失敗の直後 | 止まらない | 失敗の原因を Claude に伝える |
PostToolBatch |
並列ツール呼び出しが1バッチ解決した後 | 止まる(次のモデル呼び出し前にループ停止) | バッチ単位の検査 |
UserPromptSubmit |
プロンプト送信直後・処理前 | 止まる(プロンプトを破棄) | 入力の検証・コンテキスト注入 |
Stop |
Claude が応答を終えたとき | 止まる(停止させず会話を続行) | テスト完走の強制 |
SubagentStop |
サブエージェントの終了時 | 止まる | サブエージェントの品質ゲート |
SessionStart |
セッション開始・再開時 | 止まらない(stderr はユーザーにのみ表示) | コンテキスト読み込み・環境変数の永続化 |
Notification |
通知送出時 | 止まらない(終了コードは無視) | Slack・デスクトップ通知への転送 |
PermissionRequest |
権限確認が必要になったとき | 止まらない(decision オブジェクトで拒否する) |
承認の自動化 |
残りは Setup、UserPromptExpansion、PermissionDenied、MessageDisplay、SubagentStart、TaskCreated、TaskCompleted、StopFailure、TeammateIdle、InstructionsLoaded、ConfigChange、CwdChanged、DirectoryAdded、FileChanged、WorktreeCreate、WorktreeRemove、PreCompact、PostCompact、PreModelSwitch、PostModelSwitch、Elicitation、ElicitationResult、SessionEnd。合計33件が、2026年9月3日時点の公式リファレンスのイベント一覧に載っている数だ。
ハンドラー側も、いまは「シェルコマンド」だけではない。
| type | 何が動くか | 結果の返し方 | 既定タイムアウト |
|---|---|---|---|
command |
シェルコマンド/スクリプト | 終了コードと stdout | 600秒 |
http |
指定URLへ JSON を POST | レスポンスボディの JSON | 600秒 |
mcp_tool |
接続済み MCP サーバーのツール | ツールのテキスト出力 | 600秒 |
prompt |
Claude モデルによる単発判定 | ok / reason / impossible の JSON |
30秒 |
agent |
ツールを使える検証用サブエージェント(実験的) | 同上 | 60秒 |
prompt と agent はどこでも使えるわけではない。ツール系・Stop 系・プロンプト系の13イベントだけが5種類すべてに対応し、SessionStart と Setup にいたっては command と mcp_tool の2種類しか受け付けない。ここを間違えると設定は書けても発火しない。
設定はどこに書くか|settings.json・プラグイン・スキル・サブエージェント
置き場所がそのままスコープになる。
| 置き場所 | スコープ | 共有 |
|---|---|---|
~/.claude/settings.json |
自分の全プロジェクト | 不可(自分のマシン限定) |
.claude/settings.json |
そのプロジェクト | 可(リポジトリにコミットできる) |
.claude/settings.local.json |
そのプロジェクト | 不可(gitignore 前提) |
| マネージドポリシー設定 | 組織全体 | 可(管理者が制御) |
プラグインの hooks/hooks.json |
プラグイン有効時 | 可(プラグインに同梱) |
| スキルの frontmatter | スキル起動後そのセッション中ずっと | 可(スキルファイルに記述) |
| サブエージェントの frontmatter | そのサブエージェントの実行中のみ | 可(エージェントファイルに記述) |
覚えておきたい挙動が3つある。
1つめ、フックのエントリは階層間で「置き換え」ではなく「マージ」される。ユーザー設定とプロジェクト設定の両方に書けば両方走る。同じハンドラーを複数の設定ファイルに書いても実行は1回だが、プラグインやスキル由来の同じハンドラーは別枠として扱われる。
2つめ、一時的に全部止めるスイッチは disableAllHooks のみ。個別のフックだけを設定に残したまま無効化する方法はない。プロジェクト側の設定に関係なく1回のセッションだけ止めたい場合は --settings '{"disableAllHooks": true}' を渡す。
3つめ、組織側は allowManagedHooksOnly でユーザー・プロジェクト・ローカル・プラグインのフックを全部止められる。管理者がこれを入れている環境では、自分の ~/.claude/settings.json のフックは動かない。企業導入で「手元では動くのに配布先で動かない」ときは、まずここを疑う。設定ファイルの優先順位そのものは Claude Code /config完全ガイド でも整理している。
いま何が読み込まれているかは /hooks で確認できる。各フックに種別ラベルと取得元(User Settings / Project Settings / Local Settings / Plugin Hooks / Session Hooks)が表示される読み取り専用のビューアで、追加・編集は JSON を直接直す。
matcherの評価ルール|「Edit」は正規表現ではない
ここが2026年版でいちばん誤解されている。matcher は含まれる文字の種類によって評価方法が切り替わる。
| matcher の値 | 評価のされ方 | 例 |
|---|---|---|
"*"、""、省略 |
すべてにマッチ | そのイベントの全発火で動く |
英数字・_・-・空白・,・| のみ |
完全一致(区切り文字で複数指定) | Bash は Bash のみ。Edit|Write と Edit, Write は同じ意味 |
| それ以外の文字を含む | JavaScript の正規表現(アンカーなし) | ^Notebook、mcp__memory__.* |
つまり "matcher": "Edit" は正規表現ではなく完全一致なので、NotebookEdit にはマッチしない。逆に "Edit.*" と書いた瞬間に正規表現扱いとなり、RegExp.prototype.test の部分一致で NotebookEdit も拾う。完全一致にしたいなら ^Edit$ と書く。
MCP ツールも同じ罠がある。MCP ツール名は mcp__<server>__<tool> 形式だが、mcp__memory と書くと「完全一致の文字だけ」なのでどのツールにもマッチしない。サーバー配下を全部拾いたいなら mcp__memory__.* と .* を必ず付ける。さらにプラグイン同梱の MCP サーバーはスコープ付きの名前になり、my-plugin が db というサーバーを同梱している場合のツール名は mcp__plugin_my-plugin_db__query になる。素のサーバーキーで書いた matcher は永久に発火しない。
イベントごとに「何を見て」マッチするかも違う。PreToolUse / PostToolUse / PostToolUseFailure / PermissionRequest / PermissionDenied はツール名、SessionStart は起動理由(startup / resume / clear / compact / fork)、SubagentStart と SubagentStop はエージェント型名、Notification は通知種別。そして UserPromptSubmit、Stop、PostToolBatch、TaskCreated、TaskCompleted、WorktreeCreate、WorktreeRemove、MessageDisplay、TeammateIdle には matcher 自体がない。matcher を書いても黙って無視される。
プロセス起動を減らす if フィールド
matcher で絞ったあと、さらにツールの引数まで見て絞れるのが if だ(2.1.85 / 2026年3月26日で追加)。権限ルールと同じ構文を使う。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
"args": []
}
]
}
]
}
}
npm test のときはスクリプトを起動すらしないので、フックの常時オーバーヘッドが減る。ただし if の判定はベストエフォートだ。$TOOL git push のようにコマンド名が変数展開される場合、Claude Code は中身を確定できないのでパターンに関係なくフックを実行する。逆に「絶対に通さない」を実現したい場面では、フックではなく権限システム側で deny を書くのが公式の推奨だ。
もう1つ、if にはルールを1つしか書けない。&& や || のような結合構文はないので、条件を増やすならハンドラーを分ける。ディレクトリ指定の "Edit(src/**)" は 2.1.214(2026年7月18日)以降、作業ディレクトリ直下の src のみを指す。どの深さでも拾いたければ "Edit(**/src/**)" と書く。
exit codeとJSON出力|止まる・止まらないの境界
フックの結果は、終了コードと stdout の組み合わせで決まる。ここを雰囲気で書くと「ガードのつもりが何も止めていない」状態になる。
exit 0 は成功。stdout の扱いは形で決まり、先頭が { で末尾が } のときだけ JSON として解釈される。それ以外はプレーンテキスト扱いで、UserPromptSubmit・UserPromptExpansion・SessionStart・PostModelSwitch の4イベントではそのままコンテキストに足される。JSON のつもりの出力がパースできなかった場合、2.1.248(2026年8月27日)以降は「hook error」として通知されるようになった。それ以前は黙ってプレーンテキスト扱いだったので、古い環境ほど無言で失敗する。
exit 2 がブロックだ。ブロックできるイベントでは、JSON を出していても止まる。permissionDecision: "allow" を返していても exit 2 が勝つ。ブロック理由は、JSON にブロック判断があればその reason、なければ stderr のテキストが使われる。
#!/bin/bash
# .claude/hooks/block-rm.sh — stdin の JSON を読んでコマンドを検査する
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 # 判断なし: 通常の権限フローに戻す
それ以外の終了コードは原則ブロックしない。exit 1 は Unix の慣習では失敗だが、Claude Code は「非ブロッキングエラー」として扱い、処理はそのまま進む。ポリシーを強制するフックなら必ず exit 2 を使う。例外は WorktreeCreate で、こちらは非ゼロならすべて作成失敗になる。
もう1つ静かな失敗が、スクリプトが起動できなかった場合だ。パスの打ち間違いや実行権限なしだと、シェルが 127 などで終わり「Failed with non-blocking status code:」という通知が出るだけで、対象の操作は通ってしまう。ポリシーフックを入れた直後は、この通知が出ていないかを必ず確認する。
ブロック可否はイベントで割れる。止められるのは PreToolUse、UserPromptSubmit、UserPromptExpansion、Stop、SubagentStop、TeammateIdle、TaskCreated、TaskCompleted、PostToolBatch、ConfigChange、PreCompact、PreModelSwitch、Elicitation、ElicitationResult、WorktreeCreate。PostToolUse や PermissionDenied、Notification、SessionStart は止められない。
なお、フックの出力文字列(additionalContext、systemMessage、プレーンな stdout)は10,000文字で頭打ちになり、超えた分はファイルに退避されてパスとプレビューに置き換わる。ログを丸ごと流し込む設計は避ける。
イベント別の決定フィールド|PreToolUse・PostToolUse・Stop
PreToolUse — 4つの結果と入力の書き換え
PreToolUse だけは、トップレベルの decision ではなく hookSpecificOutput に判断を入れる。
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "本番DBへの書き込みは禁止です",
"additionalContext": "現在の環境: production"
}
}
permissionDecision に入るのは "allow" / "deny" / "ask" / "defer" の4つ。reason の見え方も違い、deny のときだけ Claude に見える(allow と ask はユーザー向け、defer では無視)。複数のフックが別々の判断を返したときの優先順位は deny > defer > ask > allow。
updatedInput を返すとツールの引数そのものを差し替えられる。入力オブジェクト全体を置き換える仕様なので、変えないフィールドも含めて返す必要がある。権限ルールの評価も、Claude が送った入力ではなくフックが返した入力に対して行われる。
旧仕様のトップレベル decision / reason はこのイベントでは非推奨で、"approve" は "allow"、"block" は "deny" にマッピングされる。古いサンプルをコピーしているなら、この機会に書き換えておきたい。
権限モードとの関係も覚えておく価値がある。PreToolUse フックは権限モードの判定より前に走るので、deny を返せば bypassPermissions や --dangerously-skip-permissions でもブロックできる。逆に allow を返しても、設定側の deny ルールは越えられない。フックは制限を締める方向にしか働かない。Auto Mode が標準化された8月以降も、フックの ask は分類器の自動承認を上書きして必ずプロンプトを出す扱いになっている(2.1.211 / 2026年7月15日で修正済み)。
PostToolUse — 止められないが、伝えられる・書き換えられる
PostToolUse はトップレベルの decision: "block" と reason を使う。ただしツールはすでに実行済みなので、これは「取り消し」ではなく「ツール結果の隣に理由を添える」動作だ。Claude には元の出力も見えている。
出力そのものを置き換えたいときは updatedToolOutput を使う(2.1.121 / 2026年4月28日に全ツールへ拡張。それ以前は MCP 専用)。ただし置き換える値はツールの出力形状に合わせる必要がある。組み込みツールは構造化オブジェクトを返すので、たとえば Bash なら stdout / stderr / interrupted / isImage を持つオブジェクトで返す。形が違うと無視されて元の出力が使われる。
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"updatedToolOutput": {
"stdout": "[redacted]",
"stderr": "",
"interrupted": false,
"isImage": false
}
}
}
秘匿情報のマスキングを組む場合、送信前は PreToolUse、受信後は PostToolUse で挟むのが公式が示す型だ。ただし PostToolUse の書き換えは「Claude に見える内容」だけを変えるもので、ファイル書き込みやネットワーク送信はすでに終わっている点は誤解しないこと。OpenTelemetry のツールスパンも書き換え前の値を記録する。
Stop / SubagentStop — 「まだ終わらせない」の作法
decision: "block" と reason(block 時は必須)で停止を止められる。exit 2 でも同じ経路になり、stderr が Claude への説明として渡る。
2026年に入ってからの重要な変更が2つある。1つは 8回連続でブロックすると Claude Code 側がフックを上書きしてターンを終えること(2.1.143 / 2026年5月15日)。無限ループ防止だ。上限は CLAUDE_CODE_STOP_HOOK_BLOCK_CAP で変えられるが、そもそもフック側で stop_hook_active を見て早期 return するのが正しい書き方になる。
もう1つは、エラー扱いにせず助言だけ返す hookSpecificOutput.additionalContext(2.1.163 / 2026年6月4日)。会話は続くが、トランスクリプトには「hook error」ではなく「Stop hook feedback」として出る。「テストを流してから終わって」のような設計どおりの誘導はこちらが向く。
入力側も充実した。last_assistant_message でその回の最終応答テキストが直接取れるので、トランスクリプトファイルをパースする必要はない(トランスクリプトは非同期書き込みで最新ターンを含まないことがある)。background_tasks と session_crons(2.1.145 / 2026年5月19日)を見れば、「本当に終わった」のか「バックグラウンド待ちで止まっているだけ」かを区別できる。
実装パターン5選(コピペ可)
1. 保護ファイルの編集をブロックする
#!/bin/bash
# .claude/hooks/protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Windows のバックスラッシュ区切りを正規化してから比較する
FILE_PATH="${FILE_PATH//\\//}"
PROTECTED=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH は保護対象($pattern)です" >&2
exit 2
fi
done
exit 0
chmod +x .claude/hooks/protect-files.sh を忘れずに。matcher は Edit|Write で登録する。Windows での正規化は公式が明示している落とし穴で、tool_input.file_path は Windows ではバックスラッシュ区切りで届く。/src/ のようなスラッシュ前提の比較は永久にマッチせず、ブロックが素通りする。
なお PreToolUse はツール呼び出しのときにしか走らない。プロンプト内で @ 参照したファイルはツールを経由せずコンテキストに入るため、Read にマッチするフックでも止められない。パスを封じたいなら権限側の Read deny ルールを使う。
2. Python編集後にruffを走らせ、結果をClaudeに返す
#!/bin/bash
# .claude/hooks/ruff-check.sh
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
case "$FILE" in *.py) ;; *) exit 0 ;; esac
OUT=$(ruff check "$FILE" 2>&1) || true
[ -z "$OUT" ] && exit 0
jq -nc --arg msg "ruff の指摘: $OUT" \
'{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: $msg}}'
ここで jq -nc --arg を使っているのは意図的だ。文字列連結で JSON を組むと、値の中の引用符やバックスラッシュが壊れてパース失敗になる。公式のトラブルシュートでも「JSON エンコーダーで組め」と名指しされている。
additionalContext は system reminder としてツール結果の隣に差し込まれる。書き方にもコツがあり、命令形ではなく事実の記述にする。「この設定を必ず守れ」のような外部システム命令風の文面は Claude のプロンプトインジェクション防御に引っかかり、コンテキストとして扱われずユーザーに転送されることがある。
3. テストが通るまで止めないStopフック
#!/bin/bash
# .claude/hooks/require-tests.sh
INPUT=$(cat)
# 8回上限に当たる前に、自分が起こした継続かどうかを見る
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
if git diff --name-only HEAD | grep -q '[.]py$'; then
if ! python3 -m pytest tests/ -q; then
jq -nc '{decision:"block", reason:"pytest が失敗しています。修正してから終了してください。"}'
exit 0
fi
fi
exit 0
stop_hook_active のガードがないと、8回連続ブロックで Claude Code 側に打ち切られて警告が出る。本番パイプラインで使う前に、必ずテスト環境で動作確認してください。
4. 重い検証はバックグラウンドに逃がす(async)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/run-tests-async.sh",
"args": [],
"async": true
}
]
}
]
}
}
async: true にすると Claude を待たせずに走り、終了後の additionalContext と systemMessage が次のターンで届く。代わりに decision や permissionDecision は効かない。ブロックしたい処理を async にしてはいけない。
制約もある。async のあいだ timeout は適用されず、セッションがアイドルなら結果は次の操作まで待たされる。claude -p の非対話実行では、終了時にまだ走っているフックは kill されて cancelled で確定する。セッションより長く生かしたい処理は、フックから完全にデタッチしたプロセスとして起動する。長時間稼働のエージェント設計は Claude Agent SDK入門ガイドでも扱っている。
5. HTTPフックで検証ロジックを社内サービスに寄せる
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "http",
"url": "http://localhost:8080/hooks/pre-tool-use",
"timeout": 30,
"headers": { "Authorization": "Bearer $MY_TOKEN" },
"allowedEnvVars": ["MY_TOKEN"]
}
]
}
]
}
}
HTTP フックは 2.1.63(2026年2月28日)で追加された。ポリシー判定を1か所に集約でき、各開発者のマシンにスクリプトを配らなくて済む。注意点はステータスコードだけではブロックできないこと。非2xx はすべて非ブロッキングエラーとして扱われ、処理は続く。拒否したいなら 2xx で permissionDecision: "deny" の JSON を返す。
ヘッダーへの環境変数展開は allowedEnvVars に列挙した変数だけが対象で、載せていない変数は空文字に置換される。組織側では allowedHttpHookUrls と httpHookAllowedEnvVars で送信先と変数を絞れる。この手の統制設計は Agent Governance Toolkitの活用事例やAIエージェントの権限設計の考え方と組み合わせると整理しやすい。
ヘッドレス実行に人間の承認を挟む|deferの正確な使い方
defer は、claude -p をサブプロセスとして呼び、JSON 出力を読む統合(Agent SDK アプリや自社UI)のための値だ。非対話モード(-p)でのみ有効で、対話セッションでは警告を出して無視される。ここは旧版の記述が曖昧だったので明確にしておく。
- Claude が
AskUserQuestionを呼び、PreToolUseが発火する - フックが
permissionDecision: "defer"を返す。ツールは実行されず、プロセスはstop_reason: "tool_deferred"で終了し、保留中のツール呼び出しがトランスクリプトに残る - 呼び出し側が結果の
deferred_tool_use(id/name/input)を読み、自前のUIで質問を出して回答を待つ - 回答が得られたら
claude -p --resume <session-id>で再開する。同じツール呼び出しでPreToolUseがもう一度発火する - 今度はフックが
permissionDecision: "allow"とupdatedInput(回答入り)を返し、ツールが実行されて会話が続く
{
"type": "result",
"subtype": "success",
"stop_reason": "tool_deferred",
"session_id": "abc123",
"deferred_tool_use": {
"id": "toolu_01abc",
"name": "AskUserQuestion",
"input": { "questions": [{ "question": "Which framework?", "header": "Framework", "options": [{"label": "React"}, {"label": "Vue"}], "multiSelect": false }] }
}
}
制約は3つ。1ターンに1つのツール呼び出しのときしか効かない(並列呼び出しでは警告つきで無視される)。タイムアウトやリトライ上限はなく、セッションは cleanupPeriodDays の掃除(既定30日)まで残る。再開時に対象ツールが消えていると stop_reason: "tool_deferred_unavailable" と is_error: true で終了する。
ちなみに AskUserQuestion と ExitPlanMode は非対話モードで通常ブロックされるが、permissionDecision: "allow" と updatedInput をセットで返せば、フック側で集めた回答を使ってそのまま実行できる。allow だけでは足りない点に注意する。
プラグイン・スキル・サブエージェントに同梱する
チームに配るなら、settings.json を各自に編集させるより同梱してしまうほうが早い。
// プラグインルートの hooks/hooks.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PLUGIN_ROOT}/scripts/format.js", "--fix"]
}
]
}
]
}
}
args があるとexec formになり、シェルを経由せず実行ファイルが直接起動される(2.1.139 / 2026年5月11日で追加)。パスに空白やアポストロフィが入ってもクォート不要で、プレースホルダーは各要素にそのまま展開される。パイプや && が必要なときだけ args を省いて shell form にする。プラグイン全体の作りは Agent Plugins 1.0の解説が参考になる。
スキルとサブエージェントは frontmatter に同じ形式で書ける。
---
name: secure-operations
description: Perform operations with security checks
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/security-check.sh"
---
登録される期間が違うのが要点だ。スキルのフックは呼び出した時点で登録され、以降そのセッション中ずっと残る。1回きりにしたいなら once: true を付ける(この指定はスキル frontmatter でのみ有効で、settings.json やエージェント frontmatter では無視される)。サブエージェントのフックは実行中だけで、ここに書いた Stop は Claude Code が SubagentStop に変換する。サブエージェント側の設計は サブエージェントfork既定化の記事、スキル自体の作り方は Claude Code Skills ガイドにまとめている。
セキュリティ面では、ワークスペース信頼の扱いを押さえておく。対話セッションでは信頼ダイアログを承認するまで、自分の ~/.claude/settings.json のフックすら走らない。一方で -p や SDK セッションではダイアログが出ず、フォルダは信頼済みとして扱われる。つまり他人のリポジトリに対して claude -p を回すと、そのリポジトリの .claude/settings.json にコミットされたフックが動く。実行前に .claude/ を読むか、--settings '{"disableAllHooks": true}' で止める。
【要注意】よくある失敗パターンと回避策
失敗1: 存在しない環境変数を参照している
❌ $CLAUDE_TOOL_NAME や $CLAUDE_TOOL_INPUT_FILE_PATH をフックコマンドに書く
⭕ stdin の JSON から jq -r '.tool_name' / jq -r '.tool_input.file_path' で取る
なぜ重要か: これらの環境変数は公式リファレンスに存在しない。展開されると空文字になり、ログには空行だけが残る。「動いているように見えて何も記録していない」という一番厄介な壊れ方をする。
失敗2: PostToolUseで実行を止めようとする
❌ PostToolUse で decision: "block" を返してツール実行を止める
⭕ 止めたいなら PreToolUse。PostToolUse は理由の添付・出力の差し替え・非同期の検査に使う
なぜ重要か: PostToolUse はツール完了後に呼ばれる。ファイル書き込みもコマンド実行もネットワーク送信も済んでいる。
失敗3: exit 1 で止まると思っている
❌ 検証に失敗したら exit 1
⭕ ブロックしたいなら exit 2(または JSON の decision フィールド)
なぜ重要か: exit 1 は非ブロッキングエラー扱いで、警告は出るが操作は通る。ポリシーゲートのつもりが単なるログになる。
失敗4: シェルプロファイルの出力がJSONを壊す
❌ ~/.bashrc や ~/.zshrc に無条件の echo がある状態で shell form のコマンドフックを使う
⭕ プロファイル側を if [[ $- == *i* ]]; then ... fi で対話シェル限定にする
なぜ重要か: 出力が JSON の前に付くと先頭が { でなくなり、全体がプレーンテキスト扱いになる。exit 0 だとトランスクリプトには何も出ず、デバッグログにしか痕跡が残らない。
失敗5: matcherを正規表現だと思い込む
❌ "matcher": "mcp__memory" でサーバー配下を全部拾えると考える
⭕ "mcp__memory__.*" と書く。完全一致にしたいときは ^Edit$ のようにアンカーする
なぜ重要か: 英数字・アンダースコア・ハイフンだけの文字列は完全一致として評価される。「1つもマッチしない matcher」は静かに何もしない。
失敗6: Stopフックが8回で打ち切られる
❌ 条件が満たされるまで無条件に decision: "block" を返し続ける
⭕ stop_hook_active を見て早期 return する。助言目的なら additionalContext に切り替える
なぜ重要か: 8回連続ブロックで Claude Code が上書きしてターンを終える。フック側で収束条件を持たないと、毎回同じ警告で終わることになる。
失敗7: SessionStartに重い処理を書く
❌ SessionStart で外部APIを叩く・大きなファイルを読み込む
⭕ 軽い初期化に留める。静的な規約は CLAUDE.md、動的な情報だけフックで足す
なぜ重要か: SessionStart はセッションのたびに走り、/clear や compact でも発火する。起動が毎回重くなる。
動かないときのデバッグ手順
順番に見ていけば、たいていは2番目までで原因が割れる。
/hooksを開く — 想定のイベントの下に、想定の取得元で載っているかを見る。ここに出ていなければ JSON の文法エラー(末尾カンマ・コメント)か置き場所の間違い。ファイル変更は通常自動で拾われるが、数秒待っても反映されなければセッションを再起動する。- 手で叩く —
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh; echo $?。終了コードと stdout をその場で確認する。「command not found」なら絶対パスか${CLAUDE_PROJECT_DIR}に直す。「jq: command not found」なら jq を入れる。 - デバッグログを読む —
claude --debug-file /tmp/claude.logで起動してtail -fする。どのフックがマッチし、終了コードと stdout / stderr が何だったかまで出る。マッチングの詳細まで見たいときはCLAUDE_CODE_DEBUG_LOG_LEVEL=verboseを足す。 - トランスクリプトを見る —
Ctrl+Oで開く。成功したフックは何も表示されないのが正常で、<hook name> hook errorが出ていればパース失敗・スキーマ検証失敗・非ブロッキングエラーのどれかを文面で切り分けられる。
Hooks仕様の変遷(公式changelogの日付つき)
「ネットの解説が古い」を判断するために、主な変更を版と日付で並べておく。出典はすべて公式 changelog。
| 版 / 日付 | 変更点 |
|---|---|
| 1.0.38 / 2025年6月30日 | hooks を初リリース |
| 2.0.45 / 2025年11月18日 | PermissionRequest フックを追加 |
| 2.1.33 / 2026年2月6日 | TeammateIdle・TaskCompleted を追加 |
| 2.1.63 / 2026年2月28日 | HTTP フック(URLへ JSON を POST)を追加 |
| 2.1.83 / 2026年3月25日 | CwdChanged・FileChanged を追加 |
| 2.1.85 / 2026年3月26日 | 条件付き実行の if フィールドを追加 |
| 2.1.118 / 2026年4月23日 | type: "mcp_tool" でMCPツールを直接呼べるように |
| 2.1.121 / 2026年4月28日 | updatedToolOutput を全ツールへ拡張 |
| 2.1.139 / 2026年5月11日 | args(exec form)を追加 |
| 2.1.141 / 2026年5月13日 | terminalSequence を追加(通知・ウィンドウタイトル・ベル) |
| 2.1.143 / 2026年5月15日 | Stop フックの連続ブロックを8回で打ち切り |
| 2.1.152 / 2026年5月27日 | MessageDisplay を追加、SessionStart に sessionTitle / reloadSkills |
| 2.1.163 / 2026年6月4日 | Stop / SubagentStop に additionalContext |
| 2.1.191 / 2026年6月24日 | カンマ区切り matcher が発火しない不具合を修正 |
| 2.1.195 / 2026年6月26日 | ハイフン入り matcher の部分一致を修正(完全一致へ) |
| 2.1.214 / 2026年7月18日 | exit 2 + スキーマ不正でもブロックするよう修正、dir/** の解釈変更、fork を source: "fork" に |
| 2.1.219 / 2026年7月24日 | DirectoryAdded を追加 |
| 2.1.248 / 2026年8月27日 | JSON に見える不正 stdout を hook error として報告 |
| 2.1.251 / 2026年8月28日 | PreModelSwitch・PostModelSwitch を追加 |
| 2.1.259 / 2026年9月2日 | ブロック中の Stop フックでその回の推論が失われる不具合を修正 |
2026年3月以前に書かれた解説記事は、if も exec form も HTTP フックも扱っていない可能性が高い。設定をコピーする前に、その記事がどの版を前提にしているかを確認したい。
運営元 Uravation よりAIエージェントを構想から本番運用まで進める順番と、体制・KPIの決め方をまとめた資料を無料で公開しています。 AIエージェント導入ロードマップを受け取る(無料)
参考・出典
- Hooks reference — Claude Code Docs(参照日: 2026-09-03)
- Automate actions with hooks — Claude Code Docs(参照日: 2026-09-03)
- Claude Code settings — Claude Code Docs(参照日: 2026-09-03)
- Claude Code settings reference — Claude Code Docs(参照日: 2026-09-03)
- Plugins reference — Claude Code Docs(参照日: 2026-09-03)
- Claude Code changelog(最新確認: 2.1.259 / 2026年9月2日、参照日: 2026-09-03)
- Intercept and control agent behavior with hooks — Claude Agent SDK Docs(参照日: 2026-09-03)
- bash_command_validator_example.py — anthropics/claude-code 公式リポジトリのリファレンス実装(参照日: 2026-09-03)
MCPサーバーと組み合わせて長時間稼働のエージェントを作る話はClaude Agent SDK入門ガイドに、フックで組んだ制御を組織のポリシーとして運用する話はAgent Governance Toolkitの活用事例にまとめています。
まとめ:今日から始める3つのアクション
- 今日:
PostToolUseにjqの1行フックを入れて~/claude-audit.logを作り、/hooksで登録されていることを確認する - 今週中:
PreToolUse+if+exit 2で.envと.git/の保護フックを書き、echo '{...}' | ./hook.shの手動テストまで通す - 今月中: チームで共有する分を
.claude/settings.jsonかプラグインのhooks/hooks.jsonに移し、${CLAUDE_PLUGIN_ROOT}と exec form でパス依存を消す
ご質問・ご相談は お問い合わせフォーム からお気軽にどうぞ。
著者: 佐藤傑(さとう・すぐる)
株式会社Uravation代表取締役。早稲田大学法学部在学中に生成AIの可能性に魅了され、X(旧Twitter)で活用法を発信(@SuguruKun_ai、フォロワー10万人超)。100社以上の企業向けAI研修・導入支援を展開。著書累計3万部突破。
Claude Code Hooksのよくある質問
設定を書き始める前に迷いやすい点を、2026年9月時点の公式リファレンスに沿って整理します。
Claude Code Hooksとは何ですか?
Claude Code のライフサイクルの特定の時点で自動実行される、ユーザー定義のシェルコマンド・HTTPエンドポイント・MCPツール呼び出し・LLMプロンプト・サブエージェントのことです。2026年9月時点で33種類のイベントがあり、ターミナル・IDE拡張・デスクトップアプリ・Web版のどこで動かしても同じイベントが発火します。CLAUDE.md の指示と違って、条件が合えば必ず実行される点が最大の違いです。
Hooksの設定はどこに書きますか?
自分の全プロジェクトに効かせるなら ~/.claude/settings.json、チームで共有するならリポジトリの .claude/settings.json、自分だけの上書きなら .claude/settings.local.json です。プラグインの hooks/hooks.json、スキルやサブエージェントの frontmatter にも書けます。各階層の設定はマージされるので、上位が下位を消すことはありません。
PreToolUseとPostToolUseはどう使い分けますか?
ツールの実行を止めたい・入力を書き換えたいなら PreToolUse、実行結果に対する記録・検証・出力の差し替えなら PostToolUse です。PostToolUse はツールが完了してから走るため、decision: "block" を返しても実行そのものは取り消せません。ファイルの書き込みやコマンド実行はすでに終わっています。
Hooksが動かないときは何を見ればいいですか?
まず /hooks で想定のイベントに登録されているかを確認し、次にサンプルJSONを標準入力に流してスクリプトを手動実行し、終了コードを見ます。それでも分からなければ claude --debug-file <path> でデバッグログを取ります。マッチしたフック、終了コード、stdout と stderr がすべて記録されます。
フックでツール実行を確実にブロックできますか?
PreToolUse で exit 2 または permissionDecision: "deny" を返せばブロックできます。権限モードの判定より前に走るため、bypassPermissions でも止まります。ただし exit 1 では止まりません。またコマンドフックがタイムアウトした場合、PreToolUse ではツール呼び出しはブロックされずに通常の権限フローへ進むため、停止したフックをゲート代わりにはできません。
最終確認日: 2026年9月3日
