フックシステム
PreToolUse、PostToolUseなどのライフサイクルイベント用のイベントサブスクライバーを構築する。
学ぶこと
Claude Codeにはビルトインのパーミッションがありますが、組織にはそれ以上のものが必要です。セキュリティチームは本番設定ファイルへのすべての書き込みをブロックしたいかもしれません。コンプライアンスチームは実行されたすべてのコマンドの監査ログが必要かもしれません。プラットフォームチームはAIが変更したファイルに自動タグ付けしたいかもしれません。フックシステムがこのすべてを可能にします。
このセッションを終えると、以下がわかるようになります:
- フックとは何か、いつ発火するか
- 4つのフックイベントタイプとその目的
settings.jsonでフックを設定する方法- マッチャーパターンがどのツールがフックをトリガーするかをフィルタリングする方法
- 入出力コントラクト(JSON stdin、終了コード)
- セキュリティゲートと監査システムの構築方法
課題
ビルトインのパーミッションシステムはバイナリです:許可か拒否か。しかし、現実のポリシーはニュアンスに富んでいます:
- 「Bashを許可、ただし
rm -rfを含むコマンドはブロック」 - 「ファイル編集を許可、ただし
config/production/内のファイルは不可」 - 「すべて許可、ただしすべてのアクションを監査ファイルにログ」
- 「gitコミットを許可、ただしメッセージにチケット番号を要求」
これらのルールは標準のパーミッションUIでは表現できません。プログラム可能なポリシー適用 — ツール実行ライフサイクルの特定のポイントで実行され、アクションを検査、変更、またはブロックできるコード — が必要です。
Without hooks:
AI calls tool → Permission check (allow/deny) → Execute
With hooks:
AI calls tool → Permission check → PreToolUse hook
│
inspect, validate,
block if needed
│
▼
Execute tool
│
▼
PostToolUse hook
│
log, audit,
side effects
仕組み
フックタイプ
Claude Codeは4つのライフサイクルイベントでフックを発火させます:
┌──────────────────────────────────────────────────┐
│ Hook Lifecycle │
│ │
│ ┌─────────────┐ │
│ │ Notification │ AIが通知を生成したときに発火 │
│ │ Hook │ (ステータスメッセージ) │
│ └─────────────┘ │
│ │
│ ┌─────────────┐ │
│ │ PreToolUse │ ツール実行の前に発火 │
│ │ Hook │ ツール呼び出しをブロック可能 │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Tool │ 実際のツール実行 │
│ │ Execution │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ PostToolUse │ ツール実行の後に発火 │
│ │ Hook │ ログと副作用用 │
│ └─────────────┘ │
│ │
│ ┌─────────────┐ │
│ │ Stop │ エージェントループがターンを │
│ │ Hook │ 終了しようとするときに発火 │
│ └─────────────┘ │
│ │
└──────────────────────────────────────────────────┘
| フック | タイミング | ブロック可能? | 一般的な用途 |
|---|---|---|---|
| PreToolUse | ツール実行前 | はい | セキュリティゲート、バリデーション |
| PostToolUse | ツール実行後 | いいえ | ログ、監査、副作用 |
| Notification | ステータスメッセージ時 | いいえ | カスタム通知 |
| Stop | ターン終了前 | はい | 最終バリデーション、クリーンアップ |
設定
フックは.claude/settings.json(プロジェクトレベル)または~/.claude/settings.json(ユーザーレベル)で定義します:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "/path/to/validate-command.sh"
},
{
"matcher": "Edit",
"command": "/path/to/check-file-path.sh"
}
],
"PostToolUse": [
{
"matcher": "*",
"command": "/path/to/audit-log.sh"
}
]
}
}
各フックエントリには以下があります:
- matcher — どのツールがこのフックをトリガーするか。
"Bash"のようなツール名か、すべてのツールに対して"*"を使用。 - command — 実行するシェルコマンド。これがフックスクリプトです。
マッチャーパターン
マッチャーはどのツール呼び出しがフックをトリガーするかを決定します:
Matcher Matches
──────── ────────────────────────────
"Bash" Bashツールの呼び出しのみ
"Edit" Editツールの呼び出しのみ
"Read" Readツールの呼び出しのみ
"mcp__*" すべてのMCPツール呼び出し
"*" すべてのツール呼び出し
複数のフックが同じツール呼び出しにマッチできます。順番に実行されます。PreToolUseフックのいずれかがブロックすると、ツール呼び出しは停止されます。
入出力コントラクト
フックが発火すると、Claude CodeはコンテキストをstdinのJSONとして渡し、終了コードから判断を読み取ります:
入力(stdin):
{
"hook_type": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/build-cache"
},
"session_id": "abc-123",
"message_id": "msg-456"
}
出力(終了コード):
| 終了コード | 意味 | 効果 |
|---|---|---|
| 0 | 許可 | ツール呼び出しが通常通り続行 |
| 2 | ブロック | ツール呼び出しがブロックされ、AIに理由が伝達 |
| その他 | エラー | 許可として扱う(フェイルオープン) |
Stdout(オプション、終了コード2の場合):
ブロック時に、フックは理由をstdoutに書き込めます。この理由はAIに表示され、調整できます:
BLOCKED: Cannot delete directories outside of project root.
AIはこれをツールエラーとして受け取り、別のアプローチを試すことができます。
実行フロー(詳細)
PreToolUseフックが設定されている場合の完全なフローです:
AIが実行したい: Bash("npm install express")
│
▼
┌──────────────────┐
│ Permission Check │ 標準の許可/拒否
└────────┬─────────┘
│ (allowed)
▼
┌──────────────────┐
│ Matcher Check │ "Bash"にマッチするフックがあるか?
└────────┬─────────┘
│ (yes, matched)
▼
┌──────────────────┐
│ Run Hook Script │ プロセスを起動、JSONをstdinにパイプ
│ │
│ stdin: { │
│ "tool_name": │
│ "Bash", │
│ "tool_input": { │
│ "command": │
│ "npm install" │
│ } │
│ } │
└────────┬─────────┘
│
┌─────┴─────┐
│ │
exit 0 exit 2
(allow) (block)
│ │
▼ ▼
Execute Return error
tool to AI:
"BLOCKED: ..."
重要なポイント
フックは組織ポリシーの拡張ポイントです。 AIの指示を変更せずに、AIが従うべきルールを適用できます。
この区別は重要です。CLAUDE.mdに「本番ファイルを決して削除しない」と書けば、AIは通常従うでしょう。しかし、指示はオーバーライドされたり、圧縮後に忘れられたり、エッジケースで無視されたりする可能性があります。ファイルパスをチェックするPreToolUseフックは決定論的です — AIが何を決定しようと、バイパスされることはありません。
次のように考えてください:
| レイヤー | メカニズム | 保証 |
|---|---|---|
| CLAUDE.mdの指示 | AIが読んで従う | ベストエフォート(AIの判断) |
| パーミッションシステム | ツールごとの許可/拒否 | バイナリ、ニュアンスなし |
| フック | 呼び出しごとのカスタムコード | 決定論的、プログラム可能 |
フックは「AIに丁寧にお願いする」と「Claude Codeにハードコードする」のギャップを埋めます。ポリシー適用レイヤーです。
ハンズオン例
例1:危険なコマンドのブロック
破壊的なシェルコマンドを防ぐPreToolUseフック:
#!/bin/bash
# hooks/block-dangerous.sh
# Bash tool用のPreToolUseフック
# stdinからJSON入力を読み取り
INPUT=$(cat)
# コマンドを抽出
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# ブロックパターンを定義
BLOCKED_PATTERNS=(
"rm -rf /"
"rm -rf ~"
"DROP DATABASE"
"DROP TABLE"
"mkfs"
"> /dev/sda"
"chmod -R 777 /"
)
# 各パターンをチェック
for pattern in "${BLOCKED_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qi "$pattern"; then
echo "BLOCKED: Command contains dangerous pattern: $pattern"
exit 2
fi
done
# コマンドを許可
exit 0
例2:監査ログ
すべてのツール実行をログに記録するPostToolUseフック:
#!/bin/bash
# hooks/audit-log.sh
# すべてのツール用のPostToolUseフック
INPUT=$(cat)
# フィールドを抽出
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
SESSION=$(echo "$INPUT" | jq -r '.session_id')
# ログエントリを構築
LOG_ENTRY=$(jq -n \
--arg ts "$TIMESTAMP" \
--arg tool "$TOOL" \
--arg session "$SESSION" \
--argjson input "$(echo "$INPUT" | jq '.tool_input')" \
'{timestamp: $ts, tool: $tool, session: $session, input: $input}')
# 監査ログに追記
echo "$LOG_ENTRY" >> ~/.claude/audit.jsonl
# PostToolUseフックは常にexit 0(実行後はブロックできない)
exit 0
これにより、JSONL監査証跡が生成されます:
{"timestamp":"2026-03-14T10:00:01Z","tool":"Bash","session":"abc","input":{"command":"npm test"}}
{"timestamp":"2026-03-14T10:00:03Z","tool":"Edit","session":"abc","input":{"file_path":"src/auth.ts",...}}
{"timestamp":"2026-03-14T10:00:05Z","tool":"Read","session":"abc","input":{"file_path":"package.json"}}
例3:本番ファイルの保護
本番設定への編集を防ぐPreToolUseフック:
#!/bin/bash
# hooks/protect-prod-config.sh
# EditとWriteツール用のPreToolUseフック
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# 保護されたパスを定義
PROTECTED=(
"config/production"
".env.production"
"docker-compose.prod"
"k8s/production"
)
for protected in "${PROTECTED[@]}"; do
if echo "$FILE_PATH" | grep -q "$protected"; then
echo "BLOCKED: Cannot modify production file: $FILE_PATH. Use a staging config instead."
exit 2
fi
done
exit 0
すべてをまとめる
3つすべてのフックを含む完全なsettings.jsonです:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "/project/.claude/hooks/block-dangerous.sh"
},
{
"matcher": "Edit",
"command": "/project/.claude/hooks/protect-prod-config.sh"
},
{
"matcher": "Write",
"command": "/project/.claude/hooks/protect-prod-config.sh"
}
],
"PostToolUse": [
{
"matcher": "*",
"command": "/project/.claude/hooks/audit-log.sh"
}
]
}
}
この設定により:
- すべてのBashコマンドは実行前にブロックリストに対してチェックされる
- すべてのファイル編集は実行前に保護されたパスに対してチェックされる
- すべてのツール実行(すべてのツール)は実行後に監査ファイルにログ記録される
AIはフックの存在を認識していません。通常通りツールを呼び出します。フックがブロックした場合、AIはエラーメッセージを受け取って適応します — 通常はより安全な代替手段を選択します。
何が変わったか
| フックなし | フックあり |
|---|---|
| ポリシーは指示(ベストエフォート) | ポリシーはコード(決定論的) |
| 監査証跡なし | すべてのアクションがログ記録 |
| 保護はAIのコンプライアンスに依存 | 保護はランタイムで適用 |
| すべてのプロジェクトで同じルール | プロジェクトごとのフック設定 |
| 外部システムとの統合方法なし | フックは任意の外部APIを呼び出し可能 |
| セキュリティは信頼ベース | セキュリティは検証してから信頼 |
次のセッション
これでモジュール3:実アーキテクチャが完了です。Claude Codeを拡張可能にする3つの柱を理解しました:コントロールプロトコル(CLIとSDKの通信方法)、MCP(外部ツールのプラグイン方法)、フック(ポリシーの適用方法)。
セッション16はモジュール4:設定とカスタマイズを開始し、セッションストレージ — Claude Codeがセッション間で状態を永続化し、継続性とメモリを実現する方法 — を扱います。