Claude Code Hooks 入門 — 「お願い」を「必ず実行」に変える仕組み
はじめに:CLAUDE.md の「お願い」では足りない
CLAUDE.md に「編集後は必ず prettier をかけて」と書いても、それは LLM へのお願いにすぎません。会話が長くなれば無視されることもあります。この「お願い」を決定論的な実行に変えるのが Hooks です。
Git の pre-commit フックと発想は同じです。「コミット時に必ず lint を走らせる」のと同様に、「Claude がファイルを編集したら必ず整形する」「危険なコマンドは必ずブロックする」をモデルの判断に左右されず強制できます。
判断の目安は、「Claude に判断させたい」なら CLAUDE.md、「Claude の判断に関わらず必ず実行させたい」なら Hooks です。
イベントと 3 つのカデンス
Hooks は Claude Code のライフサイクルの特定のポイントで発火します。イベントは**発火頻度(カデンス)**の異なる 3 層に分かれます。
-
セッションレベル(1 回)—
SessionStart(環境変数セット・コンテキスト注入)、SessionEnd(クリーンアップ) -
ターンレベル(応答ごと)—
UserPromptSubmit(入力の検証)、Stop(後処理・品質ゲート) -
ツール実行レベル(毎ツール呼び出し)—
PreToolUse(ブロック・許可)、PostToolUse(出力検証)
このほかに Notification(通知)、FileChanged(ファイル変更検知)、SubagentStop(サブエージェント終了)など 30 以上のイベントがあります。頻度の高いイベントほどフックを軽量に保つのが原則です。
設定の基本
Hooks は settings.json に定義します。配置場所でスコープが決まります。
| 配置場所 | スコープ | Git 管理 |
|---|---|---|
~/.claude/settings.json |
全プロジェクト共通 | しない |
.claude/settings.json |
プロジェクト単位 | する(チーム共有) |
.claude/settings.local.json |
プロジェクト単位 | しない |
構造は「イベント → matcher グループ → ハンドラ」の 3 階層です。スクリプトは .claude/hooks/ に配置し、chmod +x を忘れずに。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git commit *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-commit-check.sh"
}
]
}
]
}
}
matcher はツール名でフィルタリングします("Bash"、"Edit|Write"、"mcp__memory__.*" 等)。if はさらにツール引数の内容で絞り込みます("Bash(git *)" なら git コマンドのときだけ発火)。if は && 連結や $() 内のサブコマンドも検査しますが、**パースできない場合は fail open(実行を許可)**するため、絶対にブロックしたいものはスクリプト内でも検証してください。
入出力の仕組み
フックスクリプトは stdin で JSON を受け取り、stdout に JSON を返し、exit code で判定を伝えます。
# stdin で受け取る JSON の例(PreToolUse)
{ "tool_name": "Bash", "tool_input": { "command": "npm test" } }
exit code のルール:
| コード | 意味 | 動作 |
|---|---|---|
0 |
異議なし | 通常の許可フローへ(承認ではない) |
2 |
ブロック | stderr が Claude へのフィードバックになる |
| その他 | 非ブロッキングエラー | 処理は続行される |
JSON 出力(exit 0 で使用)で細かく制御できます。
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "このコマンドは禁止されています"
}
}'
permissionDecision は deny / allow / ask / defer の 4 値。複数フックが同じイベントに登録されている場合は並列実行され、deny > defer > ask > allow の優先順位でマージされます。
実践パターン 5 選
パターン 1: 危険コマンドのブロック(PreToolUse)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "if": "Bash(rm *)", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-destructive.sh" }
]
}
]
}
}
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
if echo "$COMMAND" | grep -qE 'rm\s+-(rf|fr|r)\b'; then
echo "再帰的な rm はフックでブロックされました。手動で実行してください。" >&2
exit 2
fi
exit 0
if: "Bash(rm *)" で rm コマンドのときだけ発火し、スクリプト内で -rf フラグを検査します。同じ要領で git push --force や DROP TABLE もブロックできます。
パターン 2: セッション開始時のコンテキスト注入(SessionStart)
{
"hooks": {
"SessionStart": [
{ "matcher": "startup", "hooks": [{ "type": "command", "command": ".claude/hooks/inject-context.sh" }] }
]
}
}
#!/bin/bash
BRANCH=$(git branch --show-current 2>/dev/null || echo "不明")
RECENT=$(git log --oneline -5 2>/dev/null || echo "なし")
DIRTY=$(git diff --stat 2>/dev/null || echo "なし")
CONTEXT="ブランチ: ${BRANCH} / 直近: ${RECENT} / 未コミット: ${DIRTY}"
jq -n --arg ctx "$CONTEXT" --arg title "work-${BRANCH}" '{
hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: $ctx, sessionTitle: $title }
}'
stdout に書いた内容がそのまま Claude のコンテキストに追加されます。sessionTitle でブランチ名を入れると /resume で探しやすくなります。
応用: "matcher": "compact" にすると、コンテキスト圧縮後の再注入に使えます。圧縮で失われた前提(使用ツール、規約等)を動的に再注入するパターンです。
パターン 3: ファイル編集後の自動リント(PostToolUse)
{
"hooks": {
"PostToolUse": [
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": ".claude/hooks/auto-lint.sh", "timeout": 30 }] }
]
}
}
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
if [[ "$FILE_PATH" == *.ts || "$FILE_PATH" == *.tsx || "$FILE_PATH" == *.js || "$FILE_PATH" == *.jsx ]]; then
RESULT=$(npx eslint "$FILE_PATH" 2>&1) || true
if [ -n "$RESULT" ]; then
jq -n --arg msg "lint 結果 ($FILE_PATH): $RESULT" '{
hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: $msg }
}'
fi
fi
exit 0
Claude はリント結果をコンテキストとして受け取り、エラーがあれば自動で修正を試みます。
パターン 4: テストが通るまで止めない(Stop + agent)
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "agent", "prompt": "全ユニットテストが通るか検証して。テストスイートを実行し結果を確認すること。$ARGUMENTS", "timeout": 120 }
]
}
]
}
}
type: "agent" はサブエージェントを起動し、ファイル読み取りやコマンド実行を行ったうえで {"ok": true/false, "reason": "..."} で判定を返します。ok: false なら reason が次の指示として Claude に返され、作業が続行されます。CI 的な品質ゲートをローカルで実現できます。
パターン 5: サブエージェントのコスト記録(PostToolUse + Agent)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Agent",
"hooks": [
{ "type": "command", "command": "jq -c '{agent: .tool_response.agentId, model: .tool_response.resolvedModel, tokens: .tool_response.totalTokens, ms: .tool_response.totalDurationMs}' >> ~/.claude/agent-cost.jsonl" }
]
}
]
}
}
Agent ツールの tool_response に含まれるトークン数・所要時間・モデル名を JSONL に記録します。サブエージェントを多用するワークフローで「どのエージェントがどれだけコストを使っているか」を可視化できます。
権限との関係
Hooks は制限を「締める」ことはできても「緩める」ことはできません。フックが allow を返しても、permissions.deny ルールがマッチすればブロックされます。一方で PreToolUse は権限モードチェックより前に発火するため、deny を返せば --dangerously-skip-permissions でもブロック可能です。
「このパターンは常にブロック」なら permissions.deny に追加、「条件によって判定を変えたい」なら Hooks を使う、という使い分けです。
{
"permissions": {
"deny": ["Bash(rm -rf *)", "Bash(git push --force *)"]
}
}
セキュリティと限界
フックはあなたの環境の認証情報を持った状態で自動実行されます。チーム共有の .claude/settings.json をリポジトリから受け取る場合、中身のスクリプトを実行前に確認してください。組織レベルでは allowManagedHooksOnly で管理者が検証済みのフックだけに制限できます。
Hooks の限界も押さえておきましょう。
-
/コマンドやツール呼び出しはトリガーできない。「コンテキストが 90% を超えたら/compact」のような制御は不可。自動圧縮の前後にPreCompact/PostCompactで介入するのが限界 - PostToolUse ではツール実行を取り消せない。結果の検証や警告はできるが、ロールバックはできない
-
複数フックが
updatedInputを返すと非決定的。同じツールの入力を変更するフックは 1 つに絞る
まとめ
Hooks は Claude Code の振る舞いをコードで定義する仕組みです。
- PreToolUse で危険操作をブロック
- PostToolUse で編集後の自動検証・コスト記録
- SessionStart でコンテキスト注入・圧縮後の再注入
- Stop + agent でテスト通過まで作業を継続させる品質ゲート
-
permissions.denyで全モード共通のセーフティネット
まずはパターン 1(危険コマンドのブロック)かパターン 2(コンテキスト注入)から始めてみてください。1 つ設定するだけで、Claude Code との作業体験が変わるはずです。
Discussion