Claude Code Hooks 実用パターン集 — pre/post実行・テスト自動連動・通知統合の現場で使える設計
Claude Code Hooksを運用に組み込む実践パターンを解説。PreToolUse/PostToolUseの使い分け、テスト自動連動、Slack/Telegram通知統合、APIコスト監視、マルチフック合成まで。コード例付き。
エンジニアのゆとです。
Claude Code HooksのレシピやGit連携の記事は書いてきたけど、「それを実際にどう運用に組み込むか」という話はあまり書いてこなかった。
設定例は「コピペすれば動く」だとして、問題はその先だ。複数のHookが絡むとき、どう整理すれば管理コストが上がらないか。失敗したHookのデバッグはどうするか。Slack/Telegram通知と組み合わせるときの非同期設計はどう考えるか。
この記事では、「動く」より一段上の「運用できる」を目指したHooks設計の話をする。
基本的なHooksの仕組みと単体レシピは先に読んでおいてほしい。


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を通してほしい)
- セキュリティガード(シークレットが含まれるか確認)
- 実行後に自動でかけたい処理(フォーマット、ファイルインデックス更新)
- ログ・監査記録
- テスト実行(変更があった場合にのみ実行)
- 通知(完了を知らせたい)
重要なのは 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を動かす」のキモだ。

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


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"
よくある失敗パターン:
-
実行権限が付いていない:
chmod +xを忘れると「静かに失敗」する。exit codeが返らず、Hookが存在しないのと同じ動作になる -
パスの問題:
settings.jsonのパスは実行コンテキストからの相対パスになる。プロジェクトルートから実行されている前提で書く -
jqが入っていない: macOS標準には入っていない。
brew install jqを確認する -
matcherの正規表現ミス:
Edit|Write|MultiEditは正しいがEdit|Write|multiEdit(大文字小文字)は効かない -
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.json の matcher の正規表現が正しいか(大文字小文字に注意)、(3) Hook 内に HOOK_LOG=$HOME/.claude/hook_debug.log; echo "[$(date)] $0" >> $HOOK_LOG を仕込んで実際に呼ばれているか確認。これでも動かない場合は入力JSON $(cat) を jq '.' でダンプして、tool_name のマッチ条件を見直す。
Hook で jq は必須ですか?
ほぼ必須。Hook の入力は JSON で標準入力から渡されるので、tool_name や tool_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.secrets を set -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本に分けている。
関連記事

