Claude Code Hooksのコピペレシピ10選——PreToolUse・PostToolUse・Notificationの実践パターン集
Claude Code Hooksの実践的な設定パターンを10個紹介。危険コマンドブロック、自動フォーマット、Slack通知、テスト自動実行など、settings.jsonにコピペするだけで使えるレシピ集。
エンジニアのゆとです。
Claude Code Hooksの基本は別記事で解説した。今回は「とにかく動かしたい」という人向けに、コピペで使えるレシピを10個まとめる。
解説より先に動くものを手に入れたい、というスタンスで書いた。各レシピは settings.json のスニペットとスクリプト本体をセットで載せているので、上から順に使いたいものを取っていけばいい。
前提となるHooksの基本構造(StdinのJSON形式、exit codeの意味など)を把握しておきたい場合は、先にこちらを読んでおくと理解が早い。

準備が整ったら始める。
レシピ0: 共通の前準備
全レシピ共通でやること。
まずフックスクリプトを置くディレクトリを作る。
mkdir -p .claude/hooks
スクリプトを作ったら実行権限を付与するのを忘れずに。これを忘れるとhookが「静かに失敗」して、なぜ動かないか分からなくなる。
chmod +x .claude/hooks/スクリプト名.sh
jq コマンドを使うレシピが複数あるので、まだ入れていなければ。
brew install jq
あとは ~/.claude/settings.json(全プロジェクト共通)か .claude/settings.json(プロジェクト固有)に設定を書き込めばいい。プロジェクト固有のルールはプロジェクトの方に書くのが管理しやすい。
レシピ1: 危険コマンドブロック
使うイベント: PreToolUse(Bash)rm -rf / とか git push --force とか、「Claudeが自信満々に実行しようとしているけど待って」になるコマンドを止める。
.claude/hooks/block-dangerous.sh:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
DANGEROUS_PATTERNS=(
"rm -rf /"
"rm -rf ~"
"git push --force"
"git push -f"
"git reset --hard HEAD"
"DROP TABLE"
"DROP DATABASE"
"TRUNCATE TABLE"
"> /dev/sda"
"chmod -R 777 /"
":(){ :|:& };:"
)
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qi "$pattern"; then
echo "ブロック: '$pattern' を含むコマンドは実行できません。" >&2
echo "意図的に実行する場合は手動でターミナルから直接打ってください。" >&2
exit 2
fi
done
exit 0
settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/path/to/project/.claude/hooks/block-dangerous.sh"
}
]
}
]
}
}
/path/to/project は実際のプロジェクトパスに置き換える。$CLAUDE_PROJECT_DIR 変数を使ってもいい。
exit 2を返すとClaude Codeはコマンドをキャンセルして、stderrの内容をフィードバックとして受け取る。「このコマンドは実行できないので別の方法を探します」と代替策を考え始めるので、コメントに「なぜブロックしたか」を書いておくと精度が上がる。
レシピ2: ファイル保存時に自動フォーマット
使うイベント: PostToolUse(Edit|Write)Claude Codeがファイルを書いた直後に、Prettierを自動で走らせる。「AIが書いたコードのスタイルが微妙」問題が消える。
settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path // empty' | xargs -I{} npx prettier --write {} 2>/dev/null || true"
}
]
}
]
}
}
スクリプトファイルを別に作らずにワンライナーで完結させた。jq で編集されたファイルパスを取得して、そのままPrettierに渡す。
末尾の || true は、Prettierが対応していないファイル(.sh とか .json 以外とか)を渡されたときにhook全体が失敗扱いにならないようにするためのもの。
ESLintに切り替えたければ:
"command": "jq -r '.tool_input.file_path // empty' | xargs -I{} npx eslint --fix {} 2>/dev/null || true"
TypeScriptプロジェクトなら両方をセミコロンでつなげて連続実行できる:
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -n \"$FILE\" ] && npx eslint --fix \"$FILE\" 2>/dev/null; [ -n \"$FILE\" ] && npx prettier --write \"$FILE\" 2>/dev/null; exit 0"
レシピ3: テスト自動実行
使うイベント: PostToolUse(Edit|Write)ファイル編集のたびにテストを走らせる。変更が既存テストを壊していないかをClaude Codeがすぐ把握できるようになるので、修正ループが格段に速くなる。
.claude/hooks/run-tests.sh:
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# テストファイル自体やconfig変更は除外(無限ループ防止)
if echo "$FILE" | grep -qE "(test|spec|\.config)\.(ts|js|tsx|jsx)$"; then
exit 0
fi
# src/ 以下のファイルのみ対象
if ! echo "$FILE" | grep -q "^/.*src/"; then
exit 0
fi
echo "関連テストを実行中..." >&2
# Jest(TypeScript/JavaScript)
if [ -f "jest.config.js" ] || [ -f "jest.config.ts" ] || [ -f "jest.config.json" ]; then
# 変更ファイルに対応するテストファイルを探す
TEST_FILE=$(echo "$FILE" | sed 's|src/|src/|' | sed 's|\.\(ts\|tsx\|js\|jsx\)$|.test.\1|')
if [ -f "$TEST_FILE" ]; then
npx jest "$TEST_FILE" --passWithNoTests 2>&1 | tail -5 >&2
else
# 対応テストがなければ全体を走らせる(小規模プロジェクト向け)
npx jest --passWithNoTests 2>&1 | tail -10 >&2
fi
fi
# pytest(Python)
if [ -f "pytest.ini" ] || [ -f "pyproject.toml" ]; then
python3 -m pytest --tb=short -q 2>&1 | tail -10 >&2
fi
exit 0
テスト失敗でもexit 0にしているのは意図的。テスト失敗でWriteをブロックするとClaude Codeのイテレーションが詰まるため、「情報として知らせるがブロックはしない」設計にした。Claudeがstderrを読んで「テストが落ちてるので修正します」と自律的に動いてくれる。
レシピ4: Slack/Telegram通知
使うイベント: NotificationClaude Codeが入力待ち状態になったとき(処理が終わってユーザーの応答を待っているとき)に発火するイベント。長い処理を投げてから別のことをやっていても、終わったら知らせてもらえる。
Slack通知版:.claude/hooks/notify-slack.sh:
#!/bin/bash
INPUT=$(cat)
MESSAGE=$(echo "$INPUT" | jq -r '.message // "処理が完了しました"')
TITLE=$(echo "$INPUT" | jq -r '.title // "Claude Code"')
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"text\": \"*${TITLE}*: ${MESSAGE}\"}" > /dev/null
exit 0
SLACK_WEBHOOK_URL は環境変数で渡す。.env に書いてsourceするか、スクリプト内でハードコードせずに外部から注入する。
#!/bin/bash
INPUT=$(cat)
MESSAGE=$(echo "$INPUT" | jq -r '.message // "処理が完了しました"')
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d "chat_id=${TELEGRAM_CHAT_ID}&text=Claude Code: ${MESSAGE}" > /dev/null
exit 0
macOSネイティブ通知(一番シンプル):
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"終わったよ\" with title \"Claude Code\"'"
}
]
}
]
}
}
初回に通知が来ない場合は、システム設定 > 通知 から「スクリプトエディタ」の通知をオンにする。osascript -e 'display notification "test"' をターミナルで一度手動実行して、通知の許可を与えるのがコツ。
レシピ5: コミットメッセージ自動チェック
使うイベント: PreToolUse(Bash)Conventional Commits形式(feat: fix: docs: など)を強制する。チームで使う場合はこれだけでコミットログが整う。
.claude/hooks/check-commit-msg.sh:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# git commit コマンドのみ対象
if ! echo "$COMMAND" | grep -q "^git commit"; then
exit 0
fi
# -m オプションからメッセージを取得
MESSAGE=$(echo "$COMMAND" | grep -oP '(?<=-m )["\x27].*?["\x27]' | tr -d '"' | tr -d "'")
if [ -z "$MESSAGE" ]; then
exit 0
fi
# Conventional Commitsのプレフィックスチェック
VALID_PREFIXES="feat|fix|docs|style|refactor|test|chore|perf|build|ci|revert"
if ! echo "$MESSAGE" | grep -qE "^($VALID_PREFIXES)(\(.+\))?!?: .+"; then
echo "コミットメッセージの形式が不正です。" >&2
echo "" >&2
echo "正しい形式: <type>(<scope>): <description>" >&2
echo "例:" >&2
echo " feat(auth): add OAuth2 login" >&2
echo " fix: resolve null pointer exception" >&2
echo " docs: update API reference" >&2
echo "" >&2
echo "使えるtype: $VALID_PREFIXES" >&2
exit 2
fi
exit 0
Conventionalじゃなくて自分のルールを入れてもいい。「WIP:から始まるコミットをブロック」とか「日本語コミットメッセージのみ許可」とか、正規表現を変えれば何でも対応できる。
レシピ6: 機密ファイル書き込みブロック
使うイベント: PreToolUse(Write).env や credentials.json などの機密ファイルへの書き込みをClaude Codeに対してブロックする。Claudeが「効率化のために」機密ファイルをそのまま書き換えようとする事故を防ぐ。
.claude/hooks/block-sensitive-write.sh:
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
SENSITIVE_PATTERNS=(
"\.env$"
"\.env\."
"credentials\.json"
"secrets\.json"
"\.pem$"
"\.key$"
"\.p12$"
"\.pfx$"
"id_rsa"
"id_ed25519"
"\.aws/credentials"
"\.ssh/config"
"serviceAccountKey"
)
for pattern in "${SENSITIVE_PATTERNS[@]}"; do
if echo "$FILE" | grep -qE "$pattern"; then
echo "ブロック: 機密ファイルへの書き込みは禁止されています。" >&2
echo "対象ファイル: $FILE" >&2
echo "" >&2
echo "意図的に変更する場合は手動でエディタから直接編集してください。" >&2
exit 2
fi
done
exit 0
settings.json への設定は Write と Edit の両方に適用する:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "/path/to/project/.claude/hooks/block-sensitive-write.sh"
}
]
}
]
}
}
レシピ7: 大規模ファイル変更の警告
使うイベント: PreToolUse(Edit)100行以上の差分が発生しそうな変更を「確認してから進める」モードに切り替える。一気に大量変更されて「あれ、どこが変わったんだっけ」となるのを防ぐ。
.claude/hooks/large-change-warning.sh:
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // empty')
OLD_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.old_string // empty')
if [ -z "$FILE" ] || [ -z "$NEW_CONTENT" ]; then
exit 0
fi
# 差分行数を計算
NEW_LINES=$(echo "$NEW_CONTENT" | wc -l)
OLD_LINES=$(echo "$OLD_CONTENT" | wc -l)
DIFF_LINES=$(( NEW_LINES > OLD_LINES ? NEW_LINES - OLD_LINES : OLD_LINES - NEW_LINES ))
# 50行以上の純増、または変更量が100行を超えたら警告
if [ "$DIFF_LINES" -gt 50 ] || [ "$NEW_LINES" -gt 100 ]; then
echo "大規模な変更が検出されました。" >&2
echo "ファイル: $FILE" >&2
echo "変更行数(概算): 約${DIFF_LINES}行" >&2
echo "" >&2
echo "変更を続行する前に、以下を確認してください:" >&2
echo " - 意図した範囲の変更か" >&2
echo " - ロジックが壊れていないか" >&2
echo "" >&2
# exit 2 にするとブロック。exit 0 だと警告のみで続行
# 完全にブロックしたい場合は exit 2 に変更する
exit 0
fi
exit 0
このレシピはデフォルトで「警告は出すがブロックはしない」設定(exit 0)にした。stderrに書いた内容はClaude Codeに見えるので、Claudeが「大規模変更の警告が出たので分割して進めます」と判断してくれることがある。完全に止めたい場合は末尾を exit 2 に変える。
レシピ8: TypeScript型チェック自動実行
使うイベント: PostToolUse(Edit|Write).ts や .tsx ファイルを編集するたびに tsc --noEmit を走らせて、型エラーを即座に検出する。Claudeが型エラーに気づいた上で次の編集を考えてくれるので、最終的な修正ループが減る。
.claude/hooks/typecheck.sh:
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# TypeScriptファイル以外はスキップ
if ! echo "$FILE" | grep -qE "\.(ts|tsx)$"; then
exit 0
fi
# tsconfig.jsonがある場合のみ実行
if ! [ -f "tsconfig.json" ]; then
exit 0
fi
echo "TypeScript型チェック中..." >&2
# --noEmit で型チェックのみ(ビルドはしない)
TSC_OUTPUT=$(npx tsc --noEmit 2>&1)
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
echo "型エラーが見つかりました:" >&2
echo "$TSC_OUTPUT" | head -20 >&2
echo "" >&2
echo "(型エラーがある状態で処理を続けています。Claudeが修正を試みます)" >&2
fi
# 型エラーがあっても続行(blockしない)
exit 0
型チェック失敗でもブロックしない設計。Claudeがエラーを読んで自律的に修正してくれるほうが、手動で確認するより速いことが多い。レシピ3のテスト自動実行と組み合わせると「書く→型チェック→テスト」が自動で回るようになる。
レシピ9: docker-compose変更時に自動再起動
使うイベント: PostToolUse(Edit)docker-compose.yml を編集した直後に docker compose down && docker compose up -d を自動実行する。設定変更を手動でreflectするのを忘れて「なぜ変わってないんだ」となる事故が消える。
.claude/hooks/docker-restart.sh:
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# docker-compose.yml または compose.yml のみ対象
if ! echo "$FILE" | grep -qE "docker-compose\.(yml|yaml)$|compose\.(yml|yaml)$"; then
exit 0
fi
echo "docker-compose.ymlが変更されました。コンテナを再起動します..." >&2
# 既存コンテナを停止
docker compose down 2>&1 >&2
# バックグラウンドで再起動
docker compose up -d 2>&1 >&2
if [ $? -eq 0 ]; then
echo "コンテナの再起動が完了しました。" >&2
else
echo "コンテナの起動に失敗しました。docker compose logs で確認してください。" >&2
fi
exit 0
Dockerが入っていない環境では単純に exit 0 で何もしない(docker compose コマンドが見つからなければエラーになるが、exit codeは0で返るので問題ない)。
Kubernetes(kubectl apply -f)に変えたければコマンド部分を置き換えるだけ。
レシピ10: セッションログ自動記録
使うイベント: PostToolUseClaude Codeが実行した全ツール操作をタイムスタンプ付きでログファイルに記録する。「あのとき何を変えたっけ」が後から追えるようになる。
.claude/hooks/session-logger.sh:
#!/bin/bash
INPUT=$(cat)
# 必要な情報を取得
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"')
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
# ツールごとに記録する情報を変える
case "$TOOL_NAME" in
"Bash")
DETAIL=$(echo "$INPUT" | jq -r '.tool_input.command // "(no command)"')
;;
"Write"|"Edit")
DETAIL=$(echo "$INPUT" | jq -r '.tool_input.file_path // "(no path)"')
;;
"Read")
DETAIL=$(echo "$INPUT" | jq -r '.tool_input.file_path // "(no path)"')
;;
*)
DETAIL=$(echo "$INPUT" | jq -r '.tool_input | to_entries | map(.key + "=" + (.value | tostring)) | join(", ")' 2>/dev/null || echo "(no detail)")
;;
esac
# ログに追記
LOG_DIR="$HOME/.claude/session-logs"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/$(echo "$SESSION_ID" | head -c 8)-$(date +%Y%m%d).log"
echo "[$TIMESTAMP] [$TOOL_NAME] $DETAIL" >> "$LOG_FILE"
exit 0
settings.json では matcher を空にして全ツールを対象にする:
{
"hooks": {
"PostToolUse": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "/path/to/project/.claude/hooks/session-logger.sh"
}
]
}
]
}
}
ログは ~/.claude/session-logs/ に [セッションID8桁]-YYYYMMDD.log の形式で保存される。
[2026-04-15 14:23:01] [Read] /Users/yourname/project/src/index.ts
[2026-04-15 14:23:05] [Edit] /Users/yourname/project/src/index.ts
[2026-04-15 14:23:09] [Bash] npm run test
[2026-04-15 14:23:45] [Write] /Users/yourname/project/src/utils.ts
ログが育ってきたら grep "Bash.*git commit" ~/.claude/session-logs/*.log で全セッションのコミット履歴だけ抽出できる。
複数レシピを組み合わせた settings.json
よく使う組み合わせをまとめて載せておく。
個人開発・副業向け(安全重視):{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/path/to/.claude/hooks/block-dangerous.sh"
}
]
},
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "/path/to/.claude/hooks/block-sensitive-write.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path // empty' | xargs -I{} npx prettier --write {} 2>/dev/null || true"
}
]
},
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "/path/to/.claude/hooks/session-logger.sh"
}
]
}
],
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"終わったよ\" with title \"Claude Code\"'"
}
]
}
]
}
}
チーム開発向け(品質ゲート重視):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/path/to/.claude/hooks/block-dangerous.sh"
},
{
"type": "command",
"command": "/path/to/.claude/hooks/check-commit-msg.sh"
}
]
},
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "/path/to/.claude/hooks/block-sensitive-write.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path // empty' | xargs -I{} npx prettier --write {} 2>/dev/null || true"
},
{
"type": "command",
"command": "/path/to/.claude/hooks/typecheck.sh"
},
{
"type": "command",
"command": "/path/to/.claude/hooks/run-tests.sh"
}
]
}
]
}
}
複数hookは上から順に実行される。どれか一つでもexit 2を返せばその時点でブロックされる。
トラブルシュート
hookが動いていない気がするまず実行権限を確認。ls -la .claude/hooks/ を叩いて -rwxr-xr-x になっているか見る。chmod +x を忘れていると permission denied で静かに失敗する。
次に /hooks コマンドをClaude Code内で入力して、設定が読み込まれているか確認する。
~/.zshrc や ~/.bashrc に無条件でecho文が書かれていると、hookのstdinが汚染されてJSONパースに失敗する。インタラクティブシェルのみに限定する:
# 悪い例
echo "Hello, $(whoami)"
# 良い例(インタラクティブシェルのみ実行)
[[ $- == *i* ]] && echo "Hello, $(whoami)"
詳細ログを見たい
Claude Code内で Ctrl+O を押すと詳細モードになって、hookの実行ログ・終了コード・stderr出力が全部表示される。claude --debug でClaude Codeを起動しても同様の情報が出る。
まとめ
レシピ10個をまとめると:
| レシピ | イベント | 用途 |
|---|---|---|
| 1. 危険コマンドブロック | PreToolUse(Bash) | rm -rf / git push —force 等を止める |
| 2. 自動フォーマット | PostToolUse(Edit/Write) | Prettier/ESLintを自動実行 |
| 3. テスト自動実行 | PostToolUse(Edit/Write) | pytest/Jestを自動実行 |
| 4. Slack/Telegram通知 | Notification | 処理完了をスマホで受け取る |
| 5. コミットメッセージチェック | PreToolUse(Bash) | Conventional Commits強制 |
| 6. 機密ファイルブロック | PreToolUse(Write/Edit) | .env等への書き込みを止める |
| 7. 大規模変更の警告 | PreToolUse(Edit) | 100行以上の変更に警告 |
| 8. TypeScript型チェック | PostToolUse(Edit/Write) | tsc —noEmitを自動実行 |
| 9. Docker自動再起動 | PostToolUse(Edit) | compose変更後にコンテナ再起動 |
| 10. セッションログ記録 | PostToolUse | 全操作をログファイルに記録 |
どれもsettings.jsonにコピペして chmod +x するだけで動く。最初の一歩としては、レシピ1(危険コマンドブロック)かレシピ4(通知)が一番実感しやすいと思う。この2つを入れるだけでもClaude Codeとの作業感が変わる。
Hooksの基本構造から理解したい場合は、こちらの記事も参考に:




