Claude Code Hooks 完全ガイド2026:PreToolUse/PostToolUse/Stopの実践設定と活用パターン

Claude Code Hooks 完全ガイド2026:PreToolUse/PostToolUse/Stopの実践設定と活用パターン

Claude Code Hooksの全イベント種別・設定方法・exit codeの動作を体系的に解説。危険コマンドブロック・linting自動化・デスクトップ通知など実際に使える設定例を多数収録。

エンジニアのゆとです。

Claude Code Hooksを使い始めるきっかけは大体「rm -rfを実行されそうになって怖い」か「ファイルを編集したあとlintが走ってほしい」のどちらかだと思う。

実際、Hooksはその両方を解決できる。PreToolUseでツール実行前に介入し、PostToolUseで実行後にlintを走らせ、Stopで応答終了後にテストを検証できる。Claudeが何かをする前、した後、喋り終えた後の3タイミングで自分のスクリプトを割り込ませられる仕組みだ。

設定はsettings.jsonに書くだけで、シェルスクリプト・Python・HTTPエンドポイントなどを呼び出せる。一度理解すると「なぜ今まで設定していなかったのか」と思うくらいには実用的だった。

この記事でHooksの全体像を整理する。

Hooksとは何か:ライフサイクルへの割り込み

HooksはClaude Codeのライフサイクルに自分のスクリプトをフックする仕組みだ。具体的には以下のタイミングで発火する。

SessionStart(セッション開始)

UserPromptSubmit(プロンプト送信時)

PreToolUse(ツール実行前)  ← ブロック可能

[ツール実行]

PostToolUse(ツール実行後)

Stop(応答終了後)          ← ブロック可能

最重要の特性はブロック可否だ。PreToolUseとStopはブロック可能。PostToolUseはすでに実行済みなのでブロックできない。

ユースケースは大きく分けると3類型になる。

  1. 安全ガード:危険なBashコマンドやファイル操作を事前にブロック
  2. 品質自動化:ファイル編集後のlint・フォーマット・テスト実行
  3. UX改善:デスクトップ通知、ログ記録、環境の自動セットアップ

フック種別と設定方法

設定ファイルの場所

HooksはClaude Codeのsettings.jsonに記述する。スコープによって配置場所が違う。

ファイルスコープGit管理
~/.claude/settings.json全プロジェクト共通しない
.claude/settings.jsonプロジェクト固有できる
.claude/settings.local.jsonプロジェクト固有(個人設定)しない

チーム共有したいルールはプロジェクトの.claude/settings.jsonに書く。個人的な好みはsettings.local.json~/.claude/settings.jsonへ。

基本構造

設定は3層構造になっている。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/script.sh"
          }
        ]
      }
    ]
  }
}
  • イベント名PreToolUseなど)
  • matcher(どのツールに反応するか)
  • hookハンドラ(何を実行するか)

matcherの書き方

matcherはツール名でフィルタリングする。

"matcher": "Bash"           // Bashツールのみ
"matcher": "Edit|Write"     // EditまたはWrite
"matcher": "mcp__memory__.*" // memoryサーバーの全ツール(正規表現)
"matcher": "*"              // 全ツール

MCP toolの場合はmcp__サーバー名__ツール名という形式で指定する。

Hookハンドラの種類

1. command(最も基本的)

シェルコマンドを実行する。stdinでJSONを受け取り、stdoutでJSONを返す。

{
  "type": "command",
  "command": "/path/to/script.sh",
  "timeout": 60,
  "async": false
}

argsを指定するとExec形式、省略するとShell形式になる。

// Shell形式(パイプや&&が使える)
{ "command": "npm run lint && echo done" }

// Exec形式(パス展開が安全)
{ "command": "node", "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check.js"] }

${CLAUDE_PROJECT_DIR}はプロジェクトルートに展開される。

2. http(外部サービス連携)

JSONをPOSTして、レスポンスで判断を返す。

{
  "type": "http",
  "url": "http://localhost:9000/validate",
  "headers": {
    "Authorization": "Bearer ${API_TOKEN}"
  },
  "allowedEnvVars": ["API_TOKEN"]
}

チーム内の検証サーバーや監査システムに繋ぎたい場合に使う。

3. prompt(AI評価)

Claudeに評価プロンプトを送って判断させる。

{
  "type": "prompt",
  "prompt": "このBashコマンドは安全ですか?$ARGUMENTS",
  "model": "claude-opus-4-8",
  "timeout": 30
}

判断ロジックを自分で書きたくないときに便利だが、レイテンシが増える。

Exit codeとstdoutの動作

ここを理解していないとHooksが意図通り動かない。

exit code意味JSONの扱い
0成功stdoutのJSONを処理
2ブロックエラーJSONは無視、stderrの内容がClaudeに渡る
その他非ブロックエラーJSONは無視、stderrが表示される

重要なのはexit 2はツール呼び出しを拒否するが、exit 1はエラーを報告して処理を続行する点だ。

#!/bin/bash
input=$(cat)
command=$(jq -r '.tool_input.command' <<<"$input")

if echo "$command" | grep -qE 'rm -rf'; then
  echo "危険なコマンドです" >&2
  exit 2  # ブロック
fi

exit 0  # 通過

主要イベントの使い方

PreToolUse:実行前にブロック・変換

最も活用機会が多いイベント。permissionDecisionで判断を返す。

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "rm -rfは禁止しています"
  }
}

permissionDecisionの値:

  • "allow" — 実行を許可
  • "deny" — 実行を拒否
  • "ask" — ユーザーに確認ダイアログを出す
  • "defer" — 通常フローに任せる(返さなくても同じ)

入力の変換もできる。updatedInputに置換後のツール入力を渡せば、Claudeが指定したコマンドではなく書き換えたものが実行される。

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "updatedInput": { "command": "echo 'replaced'" }
  }
}

PostToolUse:実行後にlint・フォーマット

ファイル編集後の自動検証に使う。ブロックはできないが、additionalContextでClaudeに追加情報を渡せる。

{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "ESLintエラー: no-unused-vars at line 12"
  }
}

これでClaudeが自動的にlintエラーを修正する動きになる。

Python実装例:

#!/usr/bin/env python3
import json, sys, subprocess

data = json.load(sys.stdin)
file_path = data.get('tool_input', {}).get('file_path', '')

if not file_path.endswith(('.ts', '.tsx')):
    sys.exit(0)

result = subprocess.run(['npx', 'eslint', '--format=compact', file_path],
                        capture_output=True, text=True)

if result.returncode != 0:
    output = {
        "hookSpecificOutput": {
            "hookEventName": "PostToolUse",
            "additionalContext": f"ESLintエラー:\n{result.stdout[:500]}"
        }
    }
    print(json.dumps(output))

sys.exit(0)

Stop:応答完了後に検証

Claudeが返答し終えたタイミングで発火する。テスト実行やデプロイ前チェックに使う。

exit 2を返すとターンを継続させられる(エラーメッセージとともにClaudeに渡る)。

#!/bin/bash
result=$(npm test 2>&1)
exit_code=$?

if [ $exit_code -ne 0 ]; then
  echo "$result" >&2
  exit 2  # テスト失敗をClaudeに伝えてリトライを促す
fi
exit 0

Notification:デスクトップ通知

Claude Codeが通知を出すタイミングで発火する。terminalSequenceを返すとターミナルのOSCシーケンスとして送信される。

#!/bin/bash
input=$(cat)
body=$(jq -r '.message // "Needs attention"' <<<"$input")

# macOS通知(osascript)
osascript -e "display notification \"$body\" with title \"Claude Code\""

Ghostty/Warpではターミナルシーケンス方式も使える。

{
  "terminalSequence": "\033]777;notify;Claude Code;作業が完了しました\007"
}

実用的な設定例

危険コマンドブロック(最低限入れたい設定)

// .claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/bash-guard.sh"
          }
        ]
      }
    ]
  }
}
# .claude/hooks/bash-guard.sh
#!/bin/bash
set -euo pipefail

input=$(cat)
command=$(jq -r '.tool_input.command // ""' <<<"$input")

# 破壊的操作をブロック
if echo "$command" | grep -qE '(rm -rf /|truncate -s 0|> /dev/sda|dd if=)'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "破壊的コマンドはHooksによりブロックされています"
    }
  }'
  exit 0
fi

exit 0

TypeScript/Pythonファイル編集時の自動lint

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash",
            "args": ["./.claude/hooks/lint.sh"],
            "timeout": 30
          }
        ]
      }
    ]
  }
}
# .claude/hooks/lint.sh
#!/bin/bash
input=$(cat)
file=$(jq -r '.tool_input.file_path // ""' <<<"$input")

case "$file" in
  *.ts|*.tsx)
    result=$(npx eslint --format=compact "$file" 2>&1 | head -20)
    if [ -n "$result" ]; then
      jq -n --arg ctx "ESLint: $result" '{
        hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: $ctx}
      }'
    fi
    ;;
  *.py)
    result=$(python3 -m ruff check "$file" 2>&1 | head -10)
    if [ -n "$result" ]; then
      jq -n --arg ctx "Ruff: $result" '{
        hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: $ctx}
      }'
    fi
    ;;
esac
exit 0

Gitコミット前の自動テスト(Stopフック)

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/test-on-stop.sh",
            "timeout": 120
          }
        ]
      }
    ]
  }
}
# .claude/hooks/test-on-stop.sh
#!/bin/bash

# ステージングにファイルがある場合のみテスト実行
staged=$(git diff --cached --name-only 2>/dev/null)
[ -z "$staged" ] && exit 0

result=$(npm test 2>&1)
exit_code=$?

if [ $exit_code -ne 0 ]; then
  echo "テスト失敗。修正してください:" >&2
  echo "$result" | tail -20 >&2
  exit 2  # Claudeにフィードバックして継続
fi

exit 0

SessionStartで環境確認

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-env.sh"
          }
        ]
      }
    ]
  }
}
# .claude/hooks/check-env.sh
#!/bin/bash
issues=()

command -v node &>/dev/null || issues+=("Node.js未インストール")
command -v docker &>/dev/null || issues+=("Docker未インストール")
[ -f ".env" ] || issues+=(".envファイルなし")

if [ ${#issues[@]} -gt 0 ]; then
  ctx=$(printf '%s\n' "${issues[@]}")
  jq -n --arg ctx "⚠️ 環境確認:\n$ctx" '{
    hookSpecificOutput: {
      hookEventName: "SessionStart",
      additionalContext: $ctx
    }
  }'
fi
exit 0

Hooksの確認・デバッグ

設定が正しく読み込まれているかは/hooksコマンドで確認できる。

/hooks
  PreToolUse [1 hook]
    [command] bash-guard.sh
      matcher: Bash
  PostToolUse [1 hook]
    [command] lint.sh
      matcher: Write|Edit

デバッグ時はMCP_TOOL_TIMEOUT環境変数でタイムアウトを延ばしたり、スクリプトにset -xを入れてトレースを出力するのが早い。

全フックを一時的に無効化したいときはsettings.json"disableAllHooks": trueを追加する。

よくある落とし穴

exit codeの混同が最も多い。exit 2はブロック用で、単なるスクリプトエラーでも意図せずブロックが発動することがある。エラー系は全部exit 1に統一してstderrに出力するのが安全。

jsonの出力を忘れるパターンも多い。exit 0でもJSONを出力しなければpermissionDecisiondefer扱いになる。明示的に許可したい場合は"allow"を返す。

timeoutのデフォルトは60秒。重いテストをStopフックで実行する場合は"timeout": 120などを明示しておかないと途中で打ち切られる。

まとめ

Hooksを使うと、Claude Codeの動作に「自分のルール」を組み込める。最低限やっておきたいのはPreToolUseでの危険コマンドガードとPostToolUseでのlint自動化の2つ。この2つだけでも開発体験がかなり変わる。

設定はプロジェクトの.claude/settings.jsonに書けばGitで共有できるので、チーム全員に同じガードが適用される。「うっかりClaudeにrm -rfを実行させてしまった」という事故を防ぐためにも、早めに設定しておくことをすすめる。

← 記事一覧に戻る