Claude Code Hooks 実用パターン集 — pre/post実行・テスト自動連動・通知統合の現場で使える設計

Claude Code Hooks 実用パターン集 — pre/post実行・テスト自動連動・通知統合の現場で使える設計

Claude Code Hooksを運用に組み込む実践パターンを解説。PreToolUse/PostToolUseの使い分け、テスト自動連動、Slack/Telegram通知統合、APIコスト監視、マルチフック合成まで。コード例付き。

エンジニアのゆとです。

Claude Code HooksのレシピやGit連携の記事は書いてきたけど、「それを実際にどう運用に組み込むか」という話はあまり書いてこなかった。

設定例は「コピペすれば動く」だとして、問題はその先だ。複数のHookが絡むとき、どう整理すれば管理コストが上がらないか。失敗したHookのデバッグはどうするか。Slack/Telegram通知と組み合わせるときの非同期設計はどう考えるか。

この記事では、「動く」より一段上の「運用できる」を目指したHooks設計の話をする。

基本的なHooksの仕組みと単体レシピは先に読んでおいてほしい。

Claude Code Hooksで開発を自動化する — 実際に使える5パターンを解説【2026年版】
Claude Code Hooksで開発を自動化する — 実際に使える5パターンを解説【2026年版】Claude Code Hooksの設定方法と実践的な使い方を解説。PreToolUse・PostToolUse・SessionStart・Notificationなど主要イベントを使った自動化レシピ5選。副業・フリーランスエンジニア向けに実用性重視でまとめた。読む →
Claude Code Hooksのコピペレシピ10選——PreToolUse・PostToolUse・Notificationの実践パターン集
Claude Code Hooksのコピペレシピ10選——PreToolUse・PostToolUse・Notificationの実践パターン集Claude Code Hooksの実践的な設定パターンを10個紹介。危険コマンドブロック、自動フォーマット、Slack通知、テスト自動実行など、settings.jsonにコピペするだけで使えるレシピ集。読む →

1. Hooksアーキテクチャを最初に決める

Hooksを適当に追加していくと、あっという間に settings.json が読めなくなる。最初に「どこに何を置くか」を決めておくと、あとの管理が全然違う。

僕が使っている構成:

.claude/
  hooks/
    pre/           # PreToolUse系スクリプト
      block-dangerous.sh
      lint-before-edit.sh
      secret-guard.sh
    post/          # PostToolUse系スクリプト
      format-after-edit.sh
      run-tests.sh
      cost-log.sh
    notify/        # 通知系スクリプト(PostToolUse/Stop)
      telegram-notify.sh
      slack-notify.sh
    lib/           # 共通関数
      common.sh
      notify.sh

lib/common.sh に共通関数を置いて、各スクリプトからsourceする。これをやっておかないと、同じエラーハンドリングコードを10個のスクリプトに書くことになる。

# .claude/hooks/lib/common.sh

# 標準入力からJSONを受け取る
read_input() {
  INPUT=$(cat)
  echo "$INPUT"
}

# ToolNameを取得
get_tool_name() {
  local input="$1"
  echo "$input" | jq -r '.tool_name // empty'
}

# tool_inputの特定フィールドを取得
get_tool_input() {
  local input="$1"
  local field="$2"
  echo "$input" | jq -r ".tool_input.$field // empty"
}

# Hookの正常終了(変更なし)
hook_passthrough() {
  exit 0
}

# Hookのブロック(実行禁止)
hook_block() {
  local reason="$1"
  echo "BLOCK: $reason" >&2
  exit 2
}

# Hookのエラー
hook_error() {
  local reason="$1"
  echo "ERROR: $reason" >&2
  exit 1
}

settings.json の書き方はこうなる。イベントごとに配列で積む形式:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/pre/block-dangerous.sh"
          },
          {
            "type": "command",
            "command": "bash .claude/hooks/pre/secret-guard.sh"
          }
        ]
      },
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/pre/lint-before-edit.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/post/format-after-edit.sh"
          },
          {
            "type": "command",
            "command": "bash .claude/hooks/post/run-tests.sh"
          }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/post/cost-log.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/notify/telegram-notify.sh"
          }
        ]
      }
    ]
  }
}

matcher の書き方はパイプ区切りでOR条件を作れる。Edit|Write|MultiEdit で「ファイル変更系ツール全部」を一括で捕捉できる。

2. PreToolUse + PostToolUse の使い分け設計

Hooksを設計するとき、「これはPreで止めるべきか、Postで後処理すべきか」で迷うことがある。

判断基準を整理すると:

PreToolUseで止めるべきもの
  • 実行自体を禁止したいもの(危険コマンド、ファイルの誤上書き)
  • 前提条件チェック(このコマンドを実行する前にlintを通してほしい)
  • セキュリティガード(シークレットが含まれるか確認)
PostToolUseでやるべきもの
  • 実行後に自動でかけたい処理(フォーマット、ファイルインデックス更新)
  • ログ・監査記録
  • テスト実行(変更があった場合にのみ実行)
  • 通知(完了を知らせたい)

重要なのは PreToolUseは同期的に動く という点だ。exit codeでClaude Codeの次のアクションが決まる。PostToolUseも同期的だが、終わった後の処理なのでブロックの概念が変わる。

# PreToolUse: exit 2 で「実行禁止 + 理由をClaudeに伝える」
# exit 1 で「エラー(Claudeに伝わらない、ログだけ)」
# exit 0 で「通過」

# PostToolUse: exit codeで動作が変わらない(後処理なので)
# stdout に出力した内容はClaude Codeのコンテキストに追加される
# これを使って「変更後の状態をClaudeに知らせる」ができる

PostToolUseのstdout活用が意外と知られていない。フォーマット後にlintの結果をstdoutに出すと、Claudeがその結果を読んで「lintエラーがあるので修正します」という動作をする。意図的にこれを使える。

3. テスト自動連動パターン

「ファイルを書き換えるたびに関連テストを自動実行する」はHooksで実装できる。ただし「全テストを毎回回す」のは重くなるので、「変更されたファイルに対応するテストだけ」を特定する設計が重要だ。

.claude/hooks/post/run-tests.sh

#!/bin/bash
source "$(dirname "$0")/../lib/common.sh"

INPUT=$(read_input)
TOOL_NAME=$(get_tool_name "$INPUT")
FILE_PATH=$(get_tool_input "$INPUT" "path")

# ファイルパスが取れない場合はスキップ
[ -z "$FILE_PATH" ] && exit 0

# テストファイル自体の変更はスキップ(テスト→テストの無限ループ防止)
if [[ "$FILE_PATH" == *".test."* ]] || [[ "$FILE_PATH" == *".spec."* ]]; then
  exit 0
fi

# 拡張子チェック: JS/TSのみ対象
if [[ ! "$FILE_PATH" =~ \.(js|ts|jsx|tsx)$ ]]; then
  exit 0
fi

# ディレクトリからテストファイルを特定
DIR=$(dirname "$FILE_PATH")
BASENAME=$(basename "$FILE_PATH" | sed 's/\.[^.]*$//')

# 対応するテストファイルを探す(パターン: src/foo.ts → src/foo.test.ts or __tests__/foo.test.ts)
TEST_PATTERNS=(
  "${FILE_PATH%.ts}.test.ts"
  "${FILE_PATH%.ts}.spec.ts"
  "${DIR}/__tests__/${BASENAME}.test.ts"
  "${DIR}/__tests__/${BASENAME}.spec.ts"
)

TEST_FILE=""
for pattern in "${TEST_PATTERNS[@]}"; do
  if [ -f "$pattern" ]; then
    TEST_FILE="$pattern"
    break
  fi
done

if [ -z "$TEST_FILE" ]; then
  # 対応するテストファイルなし → スキップ(警告だけ出す)
  echo "INFO: No test file found for $FILE_PATH" >&2
  exit 0
fi

echo "Running tests for: $TEST_FILE"

# テスト実行(vitest想定。jestなら npx jest $TEST_FILE に変える)
cd "$(git rev-parse --show-toplevel)" 2>/dev/null || cd "$DIR"
npx vitest run "$TEST_FILE" --reporter=verbose 2>&1

EXIT_CODE=$?

if [ $EXIT_CODE -ne 0 ]; then
  echo "TEST FAILED: $TEST_FILE"
  echo "Claude: テストが失敗しました。上記のエラーを確認して修正してください。"
  # exit 0 で終える(Claude Codeへの通知はstdoutで行う。exit 1/2は使わない)
fi

exit 0

このスクリプトのポイントが2つある。

1つ目:テストファイル自体を変更したときのループ防止。.test..spec. を含むファイルを書き換えたときはスキップしないと、テスト→テスト→テストの無限ループが起きる。

2つ目:テスト失敗時の exit 0。ここで exit 2(ブロック)を返すと、Claudeが「テスト失敗でアクションがブロックされた」と解釈して次の編集ができなくなる。stdoutにエラーを出して exit 0 で終えると、Claudeがその内容を読んで自律的に修正を試みる。これが「テスト駆動でClaude Codeを動かす」のキモだ。

Claude Code × TDD の正しい分業——人間が失敗テストを書き、AIが実装する「2人羽織」パターン
Claude Code × TDD の正しい分業——人間が失敗テストを書き、AIが実装する「2人羽織」パターンClaude CodeでTDD(テスト駆動開発)を回す実践ガイド。CC単独TDDの罠、人間がテストを書きCCが実装する2人羽織パターン、/testスキル活用、実例3ケース(API実装・リファクタ・バグ修正)、カバレッジ100%罠まで解説。読む →

4. Slack / Telegram 通知統合

Claude Codeにバックグラウンドで長時間作業させるとき、「終わったら教えて」の仕組みが必要になる。Stopイベント(Claudeが応答を完了したとき)に通知を入れるのが定番だ。

Telegram版(.claude/hooks/notify/telegram-notify.sh):

#!/bin/bash

# 設定(実際の値は環境変数か、op:// URI で取得する)
TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN}"
TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID}"

INPUT=$(cat)

# Stop イベントのみ(SessionStop は除外)
EVENT_TYPE=$(echo "$INPUT" | jq -r '.event_type // empty')
[ "$EVENT_TYPE" = "SessionStop" ] && exit 0

# 完了メッセージを取得(最後のAssistantメッセージ)
LAST_MESSAGE=$(echo "$INPUT" | jq -r '.transcript[-1].content // "タスク完了"' 2>/dev/null | head -c 200)

TIMESTAMP=$(date "+%H:%M")
TEXT="[CC完了 $TIMESTAMP]\n${LAST_MESSAGE}"

curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
  -H "Content-Type: application/json" \
  -d "{
    \"chat_id\": \"${TELEGRAM_CHAT_ID}\",
    \"text\": \"$(echo "$TEXT" | sed 's/"/\\"/g')\"
  }" > /dev/null 2>&1 &

# バックグラウンドで送信。Claude Codeの動作を遅延させない
exit 0

Slack版(.claude/hooks/notify/slack-notify.sh):

#!/bin/bash

SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL}"

INPUT=$(cat)
TIMESTAMP=$(date "+%H:%M")

# 作業内容のサマリー(最後のAssistantメッセージから取得)
SUMMARY=$(echo "$INPUT" | jq -r '.transcript[-1].content // "タスク完了"' 2>/dev/null | head -c 300)

PAYLOAD=$(cat <<EOF
{
  "text": "*Claude Code 完了通知* [${TIMESTAMP}]",
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "*[${TIMESTAMP}] CC完了*\n${SUMMARY}"
      }
    }
  ]
}
EOF
)

curl -s -X POST "$SLACK_WEBHOOK_URL" \
  -H "Content-Type: application/json" \
  -d "$PAYLOAD" > /dev/null 2>&1 &

exit 0

設定はこう:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/notify/telegram-notify.sh"
          }
        ]
      }
    ]
  }
}

通知スクリプトは必ず バックグラウンド実行(末尾に & する。Stopイベントでも同期的に待たれると、Claude Codeのターンが終わるのが遅くなる。curl が数百msかかることを考えると、ユーザー体験に影響が出る。

5. APIコスト監視フック

Claude Code を1日中動かしていると、気づいたら今日のAPI代が想定の倍だった、ということが起きる。Hookでコストをログに残して、閾値を超えたら通知する仕組みを作った。

.claude/hooks/post/cost-log.sh

#!/bin/bash

INPUT=$(cat)
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
LOG_FILE="$HOME/.claude/cost_log.jsonl"

# usage情報が含まれている場合のみ記録
INPUT_TOKENS=$(echo "$INPUT" | jq -r '.usage.input_tokens // 0')
OUTPUT_TOKENS=$(echo "$INPUT" | jq -r '.usage.output_tokens // 0')
CACHE_CREATION=$(echo "$INPUT" | jq -r '.usage.cache_creation_input_tokens // 0')
CACHE_READ=$(echo "$INPUT" | jq -r '.usage.cache_read_input_tokens // 0')

[ "$INPUT_TOKENS" = "0" ] && [ "$OUTPUT_TOKENS" = "0" ] && exit 0

# Sonnet 4.6 のレート($ per 1M tokens, 2026年4月時点)
# Input: $3 / Output: $15 / Cache creation: $3.75 / Cache read: $0.30
INPUT_COST=$(echo "scale=6; $INPUT_TOKENS * 3 / 1000000" | bc)
OUTPUT_COST=$(echo "scale=6; $OUTPUT_TOKENS * 15 / 1000000" | bc)
CACHE_CREATE_COST=$(echo "scale=6; $CACHE_CREATION * 3.75 / 1000000" | bc)
CACHE_READ_COST=$(echo "scale=6; $CACHE_READ * 0.30 / 1000000" | bc)
TOTAL_COST=$(echo "scale=6; $INPUT_COST + $OUTPUT_COST + $CACHE_CREATE_COST + $CACHE_READ_COST" | bc)

# JSONL形式で追記
echo "{\"timestamp\":\"$TIMESTAMP\",\"input\":$INPUT_TOKENS,\"output\":$OUTPUT_TOKENS,\"cache_creation\":$CACHE_CREATION,\"cache_read\":$CACHE_READ,\"cost_usd\":$TOTAL_COST}" >> "$LOG_FILE"

# 今日の累積コストを計算
TODAY=$(date "+%Y-%m-%d")
DAILY_COST=$(grep "^{\"timestamp\":\"$TODAY" "$LOG_FILE" 2>/dev/null | jq -s '[.[].cost_usd] | add // 0' 2>/dev/null)

# $5を超えたら警告(閾値は環境変数で変えられるようにする)
ALERT_THRESHOLD="${CC_COST_ALERT_USD:-5}"
if (( $(echo "$DAILY_COST > $ALERT_THRESHOLD" | bc -l) )); then
  echo "WARNING: 今日のClaude APIコストが $DAILY_COST USD に達しました(閾値: ${ALERT_THRESHOLD} USD)" >&2
fi

exit 0

1日の累積コストを集計するスクリプト:

# 今日のコストを確認
LOG_FILE="$HOME/.claude/cost_log.jsonl"
TODAY=$(date "+%Y-%m-%d")
grep "^{\"timestamp\":\"$TODAY" "$LOG_FILE" | jq -s '{total_calls: length, total_cost_usd: ([.[].cost_usd] | add // 0), avg_cost: ([.[].cost_usd] | add // 0) / length}'

6. シークレットガード(.env 誤コミット防止の強化版)

claude-code-hooks-git-workflow-2026 で危険コマンドブロックは書いたが、もう少し細かい「シークレット混入チェック」のパターンを追加しておく。

.claude/hooks/pre/secret-guard.sh

#!/bin/bash
source "$(dirname "$0")/../lib/common.sh"

INPUT=$(cat)
TOOL_NAME=$(get_tool_name "$INPUT")
CONTENT=""

case "$TOOL_NAME" in
  "Write"|"Edit"|"MultiEdit")
    # 書き込むコンテンツを取得
    CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // empty')
    ;;
  "Bash")
    # コマンド文字列を取得
    CONTENT=$(get_tool_input "$INPUT" "command")
    ;;
  *)
    exit 0
    ;;
esac

[ -z "$CONTENT" ] && exit 0

# シークレットパターン一覧
PATTERNS=(
  'sk-[a-zA-Z0-9]{48}'              # OpenAI APIキー
  'ghp_[a-zA-Z0-9]{36}'            # GitHub PAT (classic)
  'github_pat_[a-zA-Z0-9_]{82}'    # GitHub PAT (fine-grained)
  'AKIA[0-9A-Z]{16}'               # AWS Access Key ID
  'sk-ant-[a-zA-Z0-9\-_]{95}'     # Anthropic APIキー
  'op://[a-zA-Z0-9\-]+/'          # 1Password URI(平文書き込みを検出)
  'xoxb-[0-9\-a-zA-Z]{70}'        # Slack Bot Token
  'xoxp-[0-9\-a-zA-Z]{70}'        # Slack User Token
)

FOUND=""
for pattern in "${PATTERNS[@]}"; do
  if echo "$CONTENT" | grep -qE "$pattern" 2>/dev/null; then
    FOUND="$pattern"
    break
  fi
done

if [ -n "$FOUND" ]; then
  hook_block "シークレット検出: パターン '$FOUND' に一致するキーが含まれています。1Password VaultのURIに置き換えてください(例: op://Personal/OpenAI/api_key)"
fi

exit 0
1Password CLI × Claude Code で .env を Vault化する完全ガイド — op run・シェル統合・MCP の3パターン
1Password CLI × Claude Code で .env を Vault化する完全ガイド — op run・シェル統合・MCP の3パターン.envファイルの平文管理から脱却するための実践ガイド。1Password CLIを使ったop runによる実行時注入・fish/zshシェル統合・MCPサーバー経由の3パターンを解説。Claude Code SubAgentsとの組み合わせ、Doppler/Infisicalとの比較まで網羅。読む →
Claude Codeのパーミッションプロンプトを設計する——allowlistとsettings.jsonで確認頻度を最適化する
Claude Codeのパーミッションプロンプトを設計する——allowlistとsettings.jsonで確認頻度を最適化するClaude Codeのパーミッションプロンプトが頻繁に出て作業が止まる問題を解決する。settings.jsonのallowlist設定、プロジェクト別権限とグローバル権限の使い分け、危険コマンドは残しつつ安全なコマンドを通す設計パターンを実装例付きで解説。読む →

7. マルチフック合成パターン(PreToolUse チェーン)

複数のPreToolUseフックを一つのスクリプトに統合したい場合がある。フックを1ファイルにまとめることで、実行コストを下げてデバッグも楽になる。

.claude/hooks/pre/pre-bash-guard.sh

#!/bin/bash
# PreToolUse(Bash) の統合ガードスクリプト
# 全てのチェックをここで行う

source "$(dirname "$0")/../lib/common.sh"

INPUT=$(read_input)
COMMAND=$(get_tool_input "$INPUT" "command")

[ -z "$COMMAND" ] && exit 0

# --- チェック1: 危険コマンドブロック ---
DANGEROUS_PATTERNS=(
  "rm -rf /"
  "rm -rf ~"
  "dd if=/dev/zero"
  ":(){:|:&};:"   # フォーク爆弾
  "chmod -R 777 /"
  "git push.*--force.*main"
  "git push.*--force.*master"
  "git reset --hard HEAD~[0-9]+"
  "DROP TABLE"
  "DELETE FROM .* WHERE"
)

for pattern in "${DANGEROUS_PATTERNS[@]}"; do
  if echo "$COMMAND" | grep -qE "$pattern" 2>/dev/null; then
    hook_block "危険コマンド検出: '$pattern' パターンに一致。実行を中止します"
  fi
done

# --- チェック2: 本番環境操作の確認 ---
PROD_INDICATORS=(
  "production"
  "prod-"
  "-production"
  "staging"  # stagingもブロック対象にする場合
)

IS_PROD=false
for indicator in "${PROD_INDICATORS[@]}"; do
  if echo "$COMMAND" | grep -qi "$indicator"; then
    IS_PROD=true
    break
  fi
done

if $IS_PROD; then
  # 本番操作はブロックして人間の確認を求める
  hook_block "本番環境への操作が検出されました: '$COMMAND'\n本番操作はいずちゃんの手動確認が必要です"
fi

# --- チェック3: sudo の使用をログ ---
if echo "$COMMAND" | grep -q "sudo "; then
  TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
  echo "{\"timestamp\":\"$TIMESTAMP\",\"command\":\"$(echo "$COMMAND" | head -c 200)\"}" >> "$HOME/.claude/sudo_log.jsonl"
  echo "INFO: sudo コマンドをログに記録しました" >&2
  # ブロックはしない。ログだけ残す
fi

exit 0

settings.json ではこの1スクリプトだけ指定すればいい:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/pre/pre-bash-guard.sh"
          }
        ]
      }
    ]
  }
}

8. Hooksのデバッグ方法

Hooksが「動いていない気がする」ときのデバッグ手順。

まず、Hooksが実行されているかを確認する:

# Hook実行ログを仕込む(全フックに追加する)
HOOK_LOG="$HOME/.claude/hook_debug.log"
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
echo "[$TIMESTAMP] $(basename $0) called with tool: $(echo "$INPUT" | jq -r '.tool_name')" >> "$HOOK_LOG"

次に、入力JSONの中身を確認する:

# デバッグ用に生のINPUTをダンプ
INPUT=$(cat)
echo "$INPUT" | jq '.' >> "$HOME/.claude/hook_input_debug.jsonl"

よくある失敗パターン:

  1. 実行権限が付いていない: chmod +x を忘れると「静かに失敗」する。exit codeが返らず、Hookが存在しないのと同じ動作になる

  2. パスの問題: settings.json のパスは実行コンテキストからの相対パスになる。プロジェクトルートから実行されている前提で書く

  3. jqが入っていない: macOS標準には入っていない。brew install jq を確認する

  4. matcherの正規表現ミス: Edit|Write|MultiEdit は正しいが Edit|Write|multiEdit (大文字小文字)は効かない

  5. exit codeの混乱: PreToolUseのexit 2は「ブロック+Claudeへの通知」、exit 1は「エラー(Claudeに伝わらない)」。意図に合ったコードを使う

運用の現実:どこまでHooksに頼るか

Hooksは強力だけど、「全部Hooksでやろう」とすると管理コストが上がる。

僕が今実際に動かしているのはこの5本だけ:

  • 危険コマンドブロック(block-dangerous.sh)
  • シークレット混入チェック(secret-guard.sh)
  • ファイル変更後のフォーマット(format-after-edit.sh)
  • APIコストログ(cost-log.sh)
  • 作業完了のTelegram通知(telegram-notify.sh)

テスト自動連動(run-tests.sh)は試したが、テスト実行時間が長いプロジェクトでは「毎回待たされる」のがストレスになって外した。スポットで必要なときだけ claude run-tests と指示する方が実用的だった。

Hooksは「絶対に人間が目を光らせておきたいポイント」に絞って使うのが正解だと思っている。コードレビューのチェックリストと同じで、全部をHooksに任せようとすると本来の目的(何が重要かを明示すること)が薄まる。

FAQ

Claude Code Hooks と settings.json の permissions.deny の使い分けは?

役割が違う。permissions.deny はクライアント側で 強制ブロック(Claudeが動こうとしても実行されない)、Hooks は シェルスクリプトとして任意の処理を挟む(ブロック・通知・整形・ログ・テスト連動など何でもできる)。「絶対に止めたい」場合は permissions.deny、「整形・通知・条件分岐したい」場合は Hooks。Anthropic公式docsの言葉では「Settings rules are enforced by the client regardless of what Claude decides to do」。

PreToolUse と PostToolUse の使い分けは?

PreToolUse は ツール実行直前 — 入力をバリデーション、危険コマンドのブロック、シークレット混入チェックなど「実行を止めたい」ケース。PostToolUse は ツール実行直後 — フォーマット適用、テスト実行、ログ記録、Telegram通知など「実行結果を受けて何かする」ケース。多くの実用パターンは PostToolUse 側に寄る。

Hook が動かないときどうデバッグする?

3つを順に確認: (1) chmod +x で実行権限が付いているか、(2) settings.jsonmatcher の正規表現が正しいか(大文字小文字に注意)、(3) Hook 内に HOOK_LOG=$HOME/.claude/hook_debug.log; echo "[$(date)] $0" >> $HOOK_LOG を仕込んで実際に呼ばれているか確認。これでも動かない場合は入力JSON $(cat)jq '.' でダンプして、tool_name のマッチ条件を見直す。

Hook で jq は必須ですか?

ほぼ必須。Hook の入力は JSON で標準入力から渡されるので、tool_nametool_input.command を取り出すのに jq が使える前提。macOS には標準で入っていないので brew install jq を一発実行。jq を使いたくない場合は Python ワンライナーで代替可能だが、見た目の冗長さで jq が圧勝。

Hook で Telegram / Slack に通知する具体的なやり方は?

通知用シェルスクリプトを1本作って、各 Hook から呼び出すのが管理楽。Telegram は curl https://api.telegram.org/bot$TOKEN/sendMessage -d chat_id=$CHAT_ID -d "text=$MSG"、Slack は Incoming Webhook URL に curl -X POST -d '{"text":"$MSG"}' $WEBHOOK で送れる。BOT トークン / Webhook URL は .env.secretsset -a; source .env.secrets; set +a で読み込む(リポジトリにコミットしない)。

Hook で「テスト自動実行」をやるべき?

テスト実行時間が長い(30秒以上)プロジェクトではストレス源になる。本記事の運用例でも「Edit/Write のたびに npm test を回す」は外した。テストは claude run-tests のように スポット呼び出し、または PR 提出前の pre-push hook(git側)で回す方が実用的。Hooks で常時テスト連動するのは Lint・型チェックなど 数秒で終わる軽量チェック に限る。

PreToolUse の exit code、2 と 1 の違いは?

exit 2 は ブロック + Claudeに通知(理由をstderrに書ければClaudeが読んで対応する)。exit 1 は「エラー」だが Claudeには伝わらない(Hooksの設定エラー扱い)。ユーザーに止めたい意図を伝えたいなら 必ず exit 2 + stderr にメッセージ。意図に合わない exit code を使うと Claude が「なぜブロックされたか分からないまま再試行」する事故が起きる。

1つの Hook で複数のチェックを兼ねるべき?それとも分割?

分割推奨。1 Hook = 1責務にすると、(1)デバッグが楽(どこで失敗したか即特定)、(2)チェーン構成が柔軟(順序を settings.json で並べ替えるだけ)、(3)他プロジェクトに流用しやすい。本記事の運用例も block-dangerous / secret-guard / format-after-edit / cost-log / telegram-notify の5本に分けている。

関連記事

Claude Code HooksでGitワークフローを自動化する — commit前後の品質ゲートを実装する
Claude Code HooksでGitワークフローを自動化する — commit前後の品質ゲートを実装するClaude Code HooksをGitワークフローに組み込む実践ガイド。commitトリガーでのlint/test自動実行、危険コマンドのブロック、コミットメッセージの自動生成、git push前のセキュリティチェックなど、実際に動くコード例付きで解説。読む →
Claude Codeで「副業の全自動化」を作った — Skills・Hooks・MCP・定期実行の実践構成
Claude Codeで「副業の全自動化」を作った — Skills・Hooks・MCP・定期実行の実践構成Claude CodeのSkills・Hooks・MCP・LaunchAgentを組み合わせて、GA4分析から記事テーマ決定・サムネ生成・note下書き投稿・X宣伝・Search Consoleへのインデックス通知まで全自動化した実践構成をコード付きで解説。読む →
← 記事一覧に戻る