Claude Code Hooks実践ガイド — 全24イベント解説・セキュリティゲート・defer承認ワークフロー・MCP連携の本番パターン集
Hooks とは何か — CLAUDE.md・permissions との責務分担
Hooks の位置づけ:「いつ・何を・どう制御するか」のレイヤー
Claude Code の制御機構は3層に分かれる。CLAUDE.md は「何を知っているか」を伝える静的知識層。Permission Mode は allow/deny の二値で操作全体を制御する粒度層。そして Hooks は「特定の条件を満たしたとき、特定のロジックを実行する」イベント駆動の自動化層だ。
Git Hooks との類似は意図的な設計だろう。ライフサイクルイベントにスクリプトを紐づける構造は同じだが、Claude Code Hooks には permissionDecision による制御フロー変更(allow / deny / ask / defer の4値)と、4種のハンドラタイプ(後述)という独自の拡張がある。
CLAUDE.md = 知識、permissions = 粒度、Hooks = 自動化ロジック
設定ファイルの配置場所と優先順位を押さえておこう。
| 優先順位 | 配置場所 | スコープ |
|---|---|---|
| 1(最高) | Managed policy settings | Enterprise組織全体 |
| 2 | .claude/settings.local.json | プロジェクト(gitignore対象) |
| 3 | .claude/settings.json | プロジェクト(コミット可) |
| 4(最低) | ~/.claude/settings.json | グローバル(全プロジェクト) |
複数階層に同一イベントの Hook が定義されている場合、すべてが実行される。deny が1つでもあれば deny が優先される(後述の決定優先順位を参照)。
最小限の設定例として、Bash ツールで rm を含むコマンドをブロックする Hook を示す。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.command // empty' | grep -qE '\\brm\\b' && echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"rm command is blocked by security policy\"}}' || true"
}
]
}
]
}
}
全26イベント × 4ハンドラタイプ 使い分けマトリクス
ライフサイクル全体図
Hook イベントはセッションのライフサイクルに沿って発火する。大きく8フェーズに分類できる。
セッション系(6イベント)
| イベント | 発火タイミング | matcher |
|---|---|---|
SessionStart | セッション開始・再開 | startup, resume, clear, compact |
SessionEnd | セッション終了 | clear, resume, logout, prompt_input_exit 等 |
InstructionsLoaded | CLAUDE.md / rules ファイル読み込み時 | session_start, nested_traversal 等 |
ConfigChange | 設定ファイル変更時 | user_settings, project_settings 等 |
CwdChanged | 作業ディレクトリ変更時 | なし |
FileChanged | 監視ファイル変更時 | リテラルファイル名 |
プロンプト系(2イベント)
| イベント | 発火タイミング | ブロック可否 |
|---|---|---|
UserPromptSubmit | ユーザーがプロンプト送信後、処理前 | exit 2 でブロック |
Stop | Claude が応答完了時 | exit 2 でブロック |
ツール実行系(6イベント)
| イベント | 発火タイミング | ブロック可否 |
|---|---|---|
PreToolUse | ツール実行前 | permissionDecision で制御 |
PermissionRequest | 権限ダイアログ表示時 | decision.behavior で制御 |
PermissionDenied | Auto Mode がツール拒否時 | リトライ可能 |
PostToolUse | ツール成功後 | exit 2 |
PostToolUseFailure | ツール失敗後 | 不可 |
StopFailure | APIエラーでターン終了時 | 不可 |
サブエージェント・チーム系(3イベント)
| イベント | 発火タイミング | matcher |
|---|---|---|
SubagentStart | サブエージェント生成時 | エージェントタイプ |
SubagentStop | サブエージェント完了時 | エージェントタイプ |
TeammateIdle | チームメイトがアイドル状態に入った時 | なし |
タスク系(2イベント): TaskCreated / TaskCompleted
Worktree系(2イベント): WorktreeCreate / WorktreeRemove
コンパクト系(2イベント): PreCompact / PostCompact(matcher: manual / auto)
通知系(1イベント): Notification(Claude Codeが通知を送信する際に発火)
MCP Elicitation系(2イベント): Elicitation / ElicitationResult(matcher: MCPサーバー名)
4ハンドラタイプの選定基準
| タイプ | 実行方式 | 追加コスト | ユースケース |
|---|---|---|---|
| Command | シェルコマンド | なし | 正規表現マッチ、ファイル操作、ログ記録 |
| HTTP | URL に POST | なし(外部サーバー必要) | Slack 通知、外部監査ログ、Webhook連携 |
| Prompt | Claude 単発判定 | トークン消費あり | 自然言語ルール評価、曖昧な条件判定 |
| Agent | サブエージェント起動 | トークン消費大 | コードベース状態を参照する重量級検証 |
判断フローはシンプルだ。正規表現で判定できるなら Command。外部システムに通知するなら HTTP。自然言語で判定したいなら Prompt。コードを読んで判定が必要なら Agent。Prompt と Agent はトークンコストが発生するため、PostToolUse のような高頻度イベントに設定するとコストが跳ねる点に注意が必要だ。
イベント発火順を確認するデバッグ用 Hook を貼っておく。
{
"hooks": {
"PreToolUse": [{ "hooks": [{ "type": "command", "command": "echo \"$(date '+%H:%M:%S') PreToolUse $(jq -r '.tool_name' 2>/dev/null)\" >> /tmp/hooks.log" }] }],
"PostToolUse": [{ "hooks": [{ "type": "command", "command": "echo \"$(date '+%H:%M:%S') PostToolUse $(jq -r '.tool_name' 2>/dev/null)\" >> /tmp/hooks.log" }] }],
"Stop": [{ "hooks": [{ "type": "command", "command": "echo \"$(date '+%H:%M:%S') Stop\" >> /tmp/hooks.log" }] }],
"SessionStart": [{ "hooks": [{ "type": "command", "command": "echo \"$(date '+%H:%M:%S') SessionStart\" >> /tmp/hooks.log" }] }],
"SessionEnd": [{ "hooks": [{ "type": "command", "command": "echo \"$(date '+%H:%M:%S') SessionEnd\" >> /tmp/hooks.log" }] }]
}
}
セキュリティゲート設計 — PreToolUse で守る本番パターン
permissionDecision の4値
PreToolUse(と PermissionRequest)でのみ返却可能な permissionDecision は、厳密な優先順位を持つ。
deny > defer > ask > allow
複数の Hook が異なる決定を返した場合、最も制限の厳しい決定が採用される。重要な性質として、deny は --dangerously-skip-permissions モードでもブロックが有効だ。Hooks はユーザーがバイパスできないセキュリティ境界として機能する。
パターン1:危険コマンドのブロック
#!/bin/sh
# hooks/block-dangerous.sh
# stdin から JSON を読み取り、危険なコマンドをブロック
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
if [ "$TOOL_NAME" = "Bash" ]; then
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# rm -rf, git push --force, DROP TABLE をブロック
if echo "$COMMAND" | grep -qE 'rm\s+-[a-zA-Z]*r[a-zA-Z]*f|git\s+push\s+.*--force|DROP\s+TABLE'; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Destructive command blocked. Use a safer alternative."}}'
exit 0
fi
fi
パターン2:ファイルパス保護
#!/bin/sh
# hooks/protect-files.sh
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
# Edit / Write ツールで .env や credentials を保護
if [ "$TOOL_NAME" = "Edit" ] || [ "$TOOL_NAME" = "Write" ]; then
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if echo "$FILE_PATH" | grep -qE '\.(env|pem|key)$|credentials|secrets'; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Sensitive file is protected. Request manual edit instead."}}'
exit 0
fi
fi
settings.json にまとめて登録する。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "sh hooks/block-dangerous.sh" }]
},
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "sh hooks/protect-files.sh" }]
}
]
}
}
deny 時に permissionDecisionReason で代替行動を示唆すると、Claude が自律的にリカバリ手段を探してくれる。「ブロックしました」だけだと Claude が同じ操作を繰り返すことがあるので、理由は具体的に書くのがコツだ。
パターン3:Prompt ハンドラによる自然言語審査
正規表現では判定しきれない場合、Prompt ハンドラで Claude に判定させる。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "prompt",
"prompt": "以下のコマンドが本番環境に影響を与える可能性があるか判定してください。環境変数の変更、外部APIへのリクエスト、データベースへの書き込みを含む場合はブロックしてください。コマンド: $ARGUMENTS",
"statusMessage": "セキュリティ審査中..."
}
]
}
]
}
}
ただし、Prompt ハンドラは毎回トークンを消費する。Bash ツールは高頻度で呼ばれるため、本当に必要なケースに絞ること。正直、最初にこれを全ツールに設定して請求額を見たときは冷や汗をかいた。
よくあるハマりポイント
- tool_name の正確な値:
Bash,Read,Edit,Write,Glob,Grep,Agent,NotebookEditなど。大文字始まり - Hook のシェルは
/bin/sh:[[ ]]や配列は使えない。bash 固有構文はbash -c '...'で明示的に囲む permissionDecisionを返せるのはPreToolUseとPermissionRequestのみ: 他イベントで返しても無視される
defer 承認ワークフロー — CI/CD に承認ゲートを組み込む
defer の仕組み
defer は -p(ヘッドレスモード)専用の permissionDecision 値だ(v2.1.89+)。ツール実行を一時停止し、外部プロセスからの承認を待つ。
フローは以下の通り。
- ツール発火 → PreToolUse Hook が
"defer"を返す - セッションが
stop_reason: "tool_deferred"で一時停止。deferred_tool_useペイロードが返る - 外部プロセス(Slack Bot、CI/CD パイプライン等)が承認判断を収集
claude -p --resume <session-id>でセッション再開- 同一ツールの PreToolUse が再発火 → Hook が
"allow"を返す → ツール実行
制約: ターン内の単一ツール呼び出し時のみ有効。複数ツールのバッチ呼び出し時は defer が無視される。
実装例:本番デプロイ前の Slack 承認ゲート
defer を返す Hook。
#!/bin/sh
# hooks/defer-deploy.sh
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
if [ "$TOOL_NAME" = "Bash" ]; then
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -qE 'vercel deploy --prod|npm run deploy'; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"defer","permissionDecisionReason":"Production deploy requires approval"}}'
exit 0
fi
fi
承認後にセッションを再開する Node.js スクリプト。
// approve-and-resume.mjs
import { execSync } from "child_process";
const sessionId = process.argv[2];
const decision = process.argv[3]; // "allow" or "deny"
if (!sessionId || !decision) {
console.error("Usage: node approve-and-resume.mjs <session-id> <allow|deny>");
process.exit(1);
}
// セッションを再開(再開時に同じ PreToolUse が発火する)
// 承認済みフラグをファイルで管理する簡易実装
if (decision === "allow") {
// 承認フラグを書き込み、Hook 側で参照させる
const fs = await import("fs");
fs.writeFileSync(`/tmp/approved-${sessionId}`, "approved");
}
const result = execSync(
`claude -p --resume "${sessionId}" --no-input`,
{ encoding: "utf-8", timeout: 300000 }
);
console.log(result);
Hook 側を承認フラグ対応に拡張する。
#!/bin/sh
# hooks/defer-deploy.sh(承認フラグ対応版)
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
if [ "$TOOL_NAME" = "Bash" ]; then
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -qE 'vercel deploy --prod|npm run deploy'; then
# 承認フラグがあれば allow
if [ -f "/tmp/approved-${SESSION_ID}" ]; then
rm "/tmp/approved-${SESSION_ID}"
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
else
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"defer","permissionDecisionReason":"Production deploy requires Slack approval"}}'
fi
exit 0
fi
fi
実運用では承認フラグの管理を Redis や DB に移し、Slack の Interactive Message と Lambda/Worker を組み合わせて承認ボタン → resume の自動化を構築する形になる。
MCP ツール連携 — mcp<server><tool> パターンで外部ツールを制御
MCP ツールの命名規則
MCP ツールは mcp__<server>__<tool> のダブルアンダースコア区切りで tool_name に入る。<server> は mcpServers 設定のキー名がそのまま使われる。
mcp__supabase__query
mcp__github__create_pull_request
mcp__memory__create_entities
matcher フィールドは正規表現なので、サーバー単位のフィルタリングが可能だ。
実践:破壊的 MCP 操作を deny する
#!/bin/sh
# hooks/mcp-guard.sh
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
# Supabase MCP の破壊的 SQL をブロック
if echo "$TOOL_NAME" | grep -q '^mcp__supabase__'; then
QUERY=$(echo "$INPUT" | jq -r '.tool_input.query // .tool_input.sql // empty' | tr '[:lower:]' '[:upper:]')
if echo "$QUERY" | grep -qE 'INSERT|UPDATE|DELETE|DROP|ALTER|TRUNCATE'; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Write operations on Supabase require manual execution. Use SELECT only."}}'
exit 0
fi
fi
# GitHub MCP の main ブランチへの PR を ask に回す
if [ "$TOOL_NAME" = "mcp__github__create_pull_request" ]; then
BASE=$(echo "$INPUT" | jq -r '.tool_input.base // empty')
if [ "$BASE" = "main" ] || [ "$BASE" = "master" ]; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"PR to main branch requires manual approval"}}'
exit 0
fi
fi
settings.json への登録は matcher でサーバー名を指定する。
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__supabase__.*|mcp__github__.*",
"hooks": [{ "type": "command", "command": "sh hooks/mcp-guard.sh" }]
}
]
}
}
HTTP ハンドラで全 MCP 呼び出しを外部監査ログに送るパターンも有効だ。
{
"matcher": "mcp__.*",
"hooks": [
{
"type": "http",
"url": "https://audit.example.com/hooks/mcp-calls",
"headers": { "Authorization": "Bearer $AUDIT_TOKEN" },
"allowedEnvVars": ["AUDIT_TOKEN"],
"timeout": 5
}
]
}
Agent Teams・Worktree 環境での Hooks 伝播挙動
SubagentStart/Stop と Hooks の継承
親エージェントの settings.json に定義された Hooks は、サブエージェントにも伝播する。グローバル設定(~/.claude/settings.json)の Hooks はすべてのエージェントに適用され、プロジェクト設定(.claude/settings.json)の Hooks は同一プロジェクト内のサブエージェントに適用される。
SubagentStart で子エージェントの起動を監査する例。
{
"hooks": {
"SubagentStart": [
{
"hooks": [
{
"type": "http",
"url": "https://hooks.slack.com/services/T.../B.../xxx",
"timeout": 5
}
]
}
]
}
}
WorktreeCreate/Remove の活用
WorktreeCreate は --worktree フラグや isolation: "worktree" で Worktree が作成されたときに発火する。Worktree 環境では .claude/settings.json が Worktree 側にもコピーされるため、プロジェクトレベルの Hook は Worktree 内でも有効だ。
TeammateIdle は Agent Teams でチームメイトがアイドル状態に入るときに発火する。長時間タスクの監視に使える。
{
"hooks": {
"TeammateIdle": [
{
"hooks": [
{
"type": "command",
"command": "echo \"[$(date)] Teammate idle\" >> /tmp/agent-team.log"
}
]
}
]
}
}
実運用 Tips:デバッグ・パフォーマンス・チーム展開
Hooks のデバッグ
Hook の入出力をファイルにダンプするラッパースクリプトが便利だ。
#!/bin/sh
# hooks/debug-wrapper.sh
# Usage: sh hooks/debug-wrapper.sh <actual-hook-script>
INPUT=$(cat)
TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // "unknown"')
# 入力をダンプ
echo "$INPUT" | jq . > "/tmp/hook_${HOOK_EVENT}_${TIMESTAMP}_input.json"
# 実際の Hook を実行
OUTPUT=$(echo "$INPUT" | sh "$1" 2>/tmp/hook_${HOOK_EVENT}_${TIMESTAMP}_stderr.log)
EXIT_CODE=$?
# 出力をダンプ
echo "$OUTPUT" > "/tmp/hook_${HOOK_EVENT}_${TIMESTAMP}_output.json"
# 結果を返す
echo "$OUTPUT"
exit $EXIT_CODE
パフォーマンスとタイムアウト
Command ハンドラのデフォルトタイムアウトは 600秒、HTTP ハンドラは timeout フィールドで指定する。タイムアウトした Hook は非ブロッキングエラーとして処理され、ツール実行は続行される。
パフォーマンス上の注意点として、PostToolUse に重い Hook を設定すると全ツール実行後に遅延が入る。async: true を設定すればバックグラウンド実行になるが、その場合は結果を待たずに次の処理に進む。
Enterprise policy との組み合わせ
チーム展開では Managed policy で Hooks を強制適用するのが定石だ。Managed policy に定義された Hook は個人の disableAllHooks: true では無効化できない。セキュリティゲートを全メンバーに強制しつつ、個人レベルの便利 Hook は自由に追加させる運用が可能になる。
Hook プロセスに渡される主要な環境変数は以下の通り。
| 変数 | 用途 |
|---|---|
CLAUDE_PROJECT_DIR | プロジェクトルートディレクトリ |
CLAUDE_ENV_FILE | 環境変数永続化ファイルのパス(SessionStart 等) |
CLAUDE_CODE_REMOTE | Web環境で "true" |
また、入力 JSON には session_id, cwd, permission_mode, hook_event_name などが共通フィールドとして含まれる。
まとめ
Hooks は Claude Code の「自由度」と「統制」を両立させる仕組みだ。Auto Mode で生産性を上げつつ、PreToolUse でセキュリティ境界を引き、defer で人間の承認を非同期に挟み、MCP マッチングで外部ツールの暴発を防ぐ。
本記事で紹介したパターンを組み合わせれば、チームで Claude Code を導入する際の「何が起きるかわからない不安」を settings.json 1つで解消できる。まずは最小限のセキュリティゲート(危険コマンドブロック + .env 保護)から始めて、運用に合わせて defer 承認や MCP 制御へ段階的に拡張していくのがおすすめだ。
