「編集したら必ず prettier をかけて」「.env は絶対に触らないで」。CLAUDE.md やプロンプトにそう書いているのに、Claude Code が守ってくれたり、忘れたりする——。チームに Claude Code を展開し始めたエンジニアほど、この「守られたり守られなかったり」のモヤモヤに突き当たります。
やっかいなのは、プロンプトを強くしても根本的には解決しないことです。指示を丁寧にすれば規約が守られる「確率」は上がりますが、あくまで確率であって保証ではありません。結局、出力を毎回自分で確認したり、フォーマットをかけ直したり、機密ファイルを触っていないかチェックしたりする羽目になります。これでは「AI に任せて楽になる」はずが、別のチェック作業を増やしているだけです。
この問題を「お願いベース」から「保証ベース」に変えるのが、Claude Code の hooks(フック) です。hooks は Claude Code のライフサイクルの特定のポイントで発火するユーザー定義のシェルコマンドで、LLM が実行するかどうかを選ぶのに依存せず、特定のアクションが常に発生することを保証します(Claude Code 公式ドキュメント「hooks でアクションを自動化する」)。フォーマットは編集のたびに必ず走り、機密ファイルへの編集は必ず止まる——そういう決定論的な振る舞いを設定できます。
本記事では、hooks がなぜ規約遵守を「運次第」から「保証」に変えられるのかという本質から出発し、業務自動化で実務的に効くイベント(PreToolUse / PostToolUse / Stop / Notification)の選び方、settings.json の書き方とチーム規約のスコープ設計、自動フォーマットと機密ファイル保護の具体的な実装例、exit code 2 と JSON 出力の使い分け、そして hooks を本番で運用するときに事故らないための「暴発させない設計」までを、エンジニア向けに実装手順として解説します。
- Claude Code hooksとは|プロンプトとの違いと「保証」になる仕組み
- まず効くhooksイベントを押さえる|PreToolUse/PostToolUse/Stop/Notification
- settings.jsonの書き方とスコープ設計|チーム規約をどこに置くか
- 実装例1|編集後の自動フォーマット・lint・テスト(PostToolUse)
- 実装例2|危険操作・機密ファイルを「必ず」止める(PreToolUse)
- ブロックの効かせ方|exit code 2とJSON出力の使い分け
- 暴発させない設計|matcher絞り込み・無限ループ・自動承認の落とし穴
- まとめ|まず入れる3つのhookと段階的な広げ方
- よくある質問(FAQ)
Claude Code hooksとは|プロンプトとの違いと「保証」になる仕組み

hooksとは何か(ライフサイクルイベントで発火するシェルコマンド)
Claude Code の hooks とは、Claude Code のライフサイクルの特定のポイントで実行されるユーザー定義のシェルコマンドです。ファイルを編集した直後、ツールを実行する前、応答を終えたとき、入力待ちになったとき——こうしたイベントに合わせて、自分で決めたコマンドを発火させられます。
最小の例として、Claude がファイルを編集したあとに自動でフォーマットをかける hook を見てみます。設定ファイルに次のブロックを追加するだけです。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
これは「Edit または Write ツールが成功した直後に、編集されたファイルへ prettier をかける」という設定です。Claude にフォーマットをお願いするのではなく、Claude が編集を終えたタイミングで Claude Code 自身が必ずコマンドを走らせます。設定例の詳しい読み解き方や、編集ファイルだけに絞る方法はのちほど解説します。ここではまず「イベントに反応してシェルコマンドが必ず走る」というのが hooks の正体だと押さえてください。
プロンプト/CLAUDE.mdとの違い|なぜ「保証」になるのか
ここが本記事で最も重要な分かれ目です。CLAUDE.md やプロンプトと hooks は、似て非なるものです。
CLAUDE.md やプロンプトに書く「編集後は必ず lint して」という指示は、LLM への“お願い” です。Claude はその指示を読んで「lint すべきだ」と判断し、たいていは実行してくれますが、コンテキストが長くなったり、別のタスクに気を取られたりすると忘れることがあります。つまり、プロンプトで制御できるのは規約遵守の「確率」であって、100% の保証ではありません。
一方 hooks は、Claude Code というプログラムの動作そのものに割り込む仕組み です。「Edit ツールが成功したら prettier を走らせる」と設定すれば、Claude がどう判断しようと、編集が成功するたびに必ずコマンドが実行されます。LLM の判断は介在しません。だからこそ「運次第」が「保証」に変わります。
観点 | CLAUDE.md / プロンプト | hooks |
|---|---|---|
本質 | LLM への指示(お願い) | 実行の保証(決定論的な割り込み) |
守られるか | 確率的(だいたい守る/たまに忘れる) | 必ず発火する |
制御の主体 | Claude(LLM)が判断 | Claude Code(プログラム)が機械的に実行 |
向く用途 | 文脈の共有・方針の伝達 | 定型処理の強制・危険操作のブロック |
この違いを理解すると、「規約をどう書けば守ってもらえるか」と悩むのをやめて、「この規約は hooks に落とすべきか、プロンプトに残すべきか」という判断に切り替えられます。記事全体を通じて、この「お願いベースから保証ベースへ」という軸で hooks を捉えていきます。
hooksが向く処理・向かない処理
ただし、何でも hooks にすればよいわけではありません。hooks はシェルコマンドによる決定論的な処理なので、「条件が明確に定義できる定型処理」に向いています。
- 向く処理(定型・機械的な判断): 編集後のフォーマット・lint・テスト実行、機密ファイル編集のブロック、危険な Bash コマンドの拒否、入力待ちの通知、セッション開始時のコンテキスト注入。いずれも「この条件なら必ずこうする」とルールで書き切れます。
- 向かない処理(曖昧な判断が必要): 「このコミットメッセージは適切か」「この実装はベストプラクティスに沿っているか」といった、文脈に応じた判断が必要なもの。こうした処理はシェルコマンドのルールでは書き切れません。
なお Claude Code には、判断が必要なケース向けに type: "prompt"(シングルターン LLM 評価)や type: "agent"(ツールアクセス付きの検証)という hook タイプも用意されています。本記事では最も基本的で需要の高い type: "command"(シェルコマンド)を中心に扱いますが、「曖昧な判断を hook にやらせたい」場合の選択肢として、よくある質問でも改めて触れます。
まず効くhooksイベントを押さえる|PreToolUse/PostToolUse/Stop/Notification
Claude Code の hook イベントは、公式リファレンスを見ると SessionStart や PreCompact、SubagentStop など20種以上あります(Claude Code 公式ドキュメント)。すべてを覚える必要はありません。業務自動化・規約強制で実務的に効くのは、まず次の4つです。
業務自動化で効く4イベント早見表
イベント | 発火タイミング | 代表的な用途 | ブロックできるか |
|---|---|---|---|
| ツール呼び出しが実行される前 | 機密ファイル編集・危険コマンドの事前ブロック | できる(exit 2 / deny) |
| ツール呼び出しが成功した後 | 編集後の自動フォーマット・lint・テスト | できる(block) |
| Claude が応答を終えたとき | やり残しチェック・作業ツリーのスキャン | できる(block) |
| Claude が通知を送るとき(入力・許可待ち等) | 入力待ちのデスクトップ通知 | できない |
この4つを「割り込みたいタイミング」と「やりたいこと」で対応づけて選ぶのが、設計の出発点になります。
PreToolUseとPostToolUseの役割分担
最も使うのが PreToolUse と PostToolUse です。両者は「ツールの前後どちらで割り込むか」で役割が分かれます。
PreToolUse(事前ブロック): ツールが実行される前に発火し、ツール呼び出しをブロックできます。「.envを編集しようとしている」「rm -rfを実行しようとしている」といった、起きてからでは遅い操作を未然に止めるのに使います。裏テーマで言えば「機密ファイルを触られる・危険コマンドを走らせかける」不安に直接効くのがこちらです。PostToolUse(事後処理): ツールが成功した後に発火します。すでに実行された後なので操作を取り消すことはできませんが、編集された結果に対してフォーマットや lint、テストを走らせるのに使います。「フォーマットを忘れる」という最頻出の悩みに効くのがこちらです。
つまり「止めたいことは PreToolUse、整えたいことは PostToolUse」と覚えておけば、最初の判断はほぼ間違いません。
Stop/Notificationで「やり忘れ」と「待ち時間」を潰す
残りの2つは、開発体験を地味に底上げします。
Stop: Claude が応答を終えるたびに発火します(ユーザーの割り込み時には発火しません)。「タスクが完了しているか」「未フォーマットのファイルが残っていないか」といった、ターンの最後にまとめて確認したいチェックに使います。後述する「Bash 経由のファイル変更を取りこぼさない」補完にも、このStopを使います。Notification: Claude が入力や許可を待っているときに発火します。長い処理を回している間に別作業へ切り替えても、入力待ちになった瞬間にデスクトップ通知が飛ぶので、ターミナルを監視し続ける必要がなくなります。
補足として、SessionStart(セッション開始・コンテキスト注入)や SubagentStop(サブエージェント完了)も状況によっては有用ですが、まずは上の4つから始めるのが現実的です。
settings.jsonの書き方とスコープ設計|チーム規約をどこに置くか

イベントの選び方が分かったら、実際の設定ファイル(settings.json)への書き方と、「その設定をどこに置くか」を押さえます。チームに規約を守らせたい場合、置き場所の設計が肝になります。
hooksブロックの基本構造(イベント / matcher / command)
hooks の設定は、次の入れ子構造になっています。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
構造を上から読むと、次のようになります。
"hooks": ルートのキー。"PostToolUse": イベント名。どのタイミングで発火させるか。配列で複数の設定を持てます。"matcher": "Edit|Write": どのツールにマッチさせるかの絞り込み。PostToolUseならツール名でフィルタします(後述)。"hooks": [...]: マッチしたときに実行する処理のリスト。"type": "command"+"command": "...": 実際に走らせるシェルコマンド。
複数のイベントを設定するときは、"PostToolUse" と "Notification" のように、同じ "hooks" オブジェクトの中にイベント名を並べて書きます。既存の "hooks" キーがある設定ファイルに追記する場合は、オブジェクト全体を置き換えず、既存のイベントの兄弟としてキーを足してください。
3つのスコープと優先順位(user / project / local)|規約はどこに置くか
hooks を書く設定ファイルは、置き場所によってスコープ(適用範囲)が変わります。チーム規約を扱ううえで最重要のポイントです。
置き場所 | スコープ | 共有 |
|---|---|---|
| すべてのプロジェクト | マシンローカル(共有されない) |
| 単一プロジェクト | リポジトリにコミット可能(チーム共有できる) |
| 単一プロジェクト | gitignore 対象(個人用・共有されない) |
このほか、組織全体に適用される管理ポリシー設定もあります(管理者が制御する設定で、後述のとおり最も強い優先順位を持ちます)。
「チームに規約を守らせたい」という裏テーマに対する答えは明確です。チーム共通の規約は .claude/settings.json に書いてリポジトリにコミットする ことです。こうすれば、そのリポジトリをクローンした全員に同じ hooks が適用され、「人によってフォーマットがバラつく」「規約を守らせきれない」という状態を、設定ファイル一つで解消できます。一方、個人的に使いたいデスクトップ通知のようなカスタムは ~/.claude/settings.json(全プロジェクト)や .claude/settings.local.json(このプロジェクトだけ・gitignore)に置けば、チームに押し付けずに済みます。
整理すると、配置の指針は次のとおりです。
- チーム規約(lint・フォーマット・機密ファイル保護) →
.claude/settings.json(コミットして全員に強制) - 個人カスタム(通知など) →
~/.claude/settings.jsonまたは.claude/settings.local.json
外部スクリプト化と /hooks での確認
command に長いシェルロジックを直接書くと、JSON のエスケープが煩雑になり、可読性も下がります。ロジックが数行を超えるなら、スクリプトファイルに切り出すのがおすすめです。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}
ポイントは2つあります。
- スクリプトは
.claude/hooks/配下に置き、chmod +x .claude/hooks/protect-files.shで実行可能にしておくこと。Claude Code が hook スクリプトを実行するには実行権限が必要です。 - パスは
$CLAUDE_PROJECT_DIR(プロジェクトルートを指す環境変数)を使って参照すること。これでどのディレクトリから起動しても、スクリプトを正しく見つけられます。
設定したら、Claude Code 上で /hooks と入力すると、登録済みの hooks をイベント別に一覧で確認できます。ただし /hooks メニューは読み取り専用なので、追加・変更・削除は設定 JSON を直接編集するか、Claude に編集を依頼してください。
実装例1|編集後の自動フォーマット・lint・テスト(PostToolUse)
ここからは具体的な実装に入ります。まず需要が最も高い「編集後の事後処理」を PostToolUse で実装します。
編集ファイルだけをフォーマットする(Edit|Write matcher + jq)
冒頭で出てきた auto-format の設定を、改めてきちんと解説します。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
仕組みはこうです。PostToolUse イベントが発火すると、Claude Code はイベント固有のデータを JSON として hook の標準入力(stdin)に渡します。この JSON には、Claude がどのツールにどんな引数を渡したかが含まれています。Edit / Write ツールの場合は tool_input.file_path に編集されたファイルのパスが入っているので、jq -r '.tool_input.file_path' でパスだけを取り出し、xargs で prettier --write に渡します。
matcher: "Edit|Write" で Edit または Write ツールに限定しているのが重要です。これがないと Read や Bash を含む全ツールの後でフォーマッターが走ってしまい、無駄な実行や予期しない挙動を招きます。「編集したファイルだけを、編集の直後に必ず整形する」が実現できます。
なお jq は JSON を解析するためのコマンドで、本記事の Bash 例でも多用します。未インストールなら macOS は brew install jq、Debian/Ubuntu は apt-get install jq で導入できます。
lint/テストを走らせ、失敗をClaudeにフィードバックして直させる
フォーマットだけでなく、lint やテストを走らせ、失敗したら Claude に直させる こともできます。PostToolUse で問題を検知したとき、hook から「ブロック」を返すと、その理由が Claude にフィードバックされ、Claude が自己修正に動きます。
ブロックの返し方は2通りあります。
- 非ゼロ終了(exit 2): スクリプトを exit 2 で終了し、標準エラー出力(stderr)にエラー内容を書くと、それが Claude へのフィードバックになります。
- JSON 出力: 標準出力(stdout)に
{"decision": "block", "reason": "..."}を返す(PostToolUse/Stopはトップレベルのdecisionフィールドを使います)。
例えば「TypeScript ファイルを編集したら型チェックを走らせ、エラーがあれば内容を Claude に返す」スクリプトは次のように書けます。
#!/bin/bash
# typecheck-on-edit.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# TypeScript ファイル以外はスキップ
case "$FILE_PATH" in
*.ts|*.tsx) ;;
*) exit 0 ;;
esac
if ! OUTPUT=$(npx tsc --noEmit 2>&1); then
echo "型チェックに失敗しました。修正してください:" >&2
echo "$OUTPUT" >&2
exit 2
fi
exit 0
これで、Claude が型エラーを残したまま編集を終えても、hook が必ず検知して Claude に差し戻すので、エラーの放置が起きにくくなります。
Bash経由の変更を取りこぼさない(Stopで作業ツリーをスキャン)
ここに一つ落とし穴があります。Claude は Edit / Write ツールだけでなく、Bash ツールでシェルコマンドを実行してファイルを変更することもあります(echo ... > file や sed -i など)。この場合、matcher: "Edit|Write" の PostToolUse では拾えません。
公式ドキュメントでも、すべてのファイル変更を確実に捕捉したい場合は、ターンごとに1回 Stop hook で作業ツリーをスキャンする 方法が案内されています。Stop は Claude が応答を終えるたびに発火するので、そこで git status --porcelain を使って変更・未追跡ファイルを洗い出し、未整形のものをまとめてフォーマットすれば、Bash 経由の変更も取りこぼしません。
#!/bin/bash
# format-on-stop.sh — Stop hook で未整形ファイルをまとめて処理
CHANGED=$(git status --porcelain | awk '{print $2}')
[ -z "$CHANGED" ] && exit 0
echo "$CHANGED" | grep -E '\.(ts|tsx|js|json)$' | xargs -r npx prettier --write
exit 0
「呼び出しごとに確実に整形したい」なら PostToolUse で Bash もマッチさせる、「ターンの最後にまとめて確認できればよい」なら Stop でスキャンする、という使い分けになります。
実装例2|危険操作・機密ファイルを「必ず」止める(PreToolUse)

ここが裏テーマの核心です。「機密ファイルを触られる」「危険なコマンドを走らせかける」——起きてからでは遅い操作を、PreToolUse で必ず止める実装を見ていきます。
機密ファイルの編集をブロックする(PreToolUse + exit 2 + stderrフィードバック)
.env や package-lock.json、.git/ 配下といった機密・重要ファイルへの編集を未然に止めます。公式ドキュメントが示す protect-files.sh を、外部スクリプトとして用意します。
#!/bin/bash
# .claude/hooks/protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
exit 2
fi
done
exit 0
chmod +x .claude/hooks/protect-files.sh で実行可能にしたうえで、PreToolUse に登録します。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}
ここでの肝は exit 2 です。PreToolUse hook が exit 2 で終了すると、ツール呼び出しがブロックされ、stderr に書いた理由が Claude へのフィードバックになります。Claude は「.env は保護されているから編集できない」と理解し、別のアプローチに切り替えます。単に失敗するのではなく、Claude が方針を立て直せる点が優れています。
危険なBashコマンドをブロックする|ifでのgit限定スコープ
ファイルだけでなく、rm -rf や drop table のような危険な Bash コマンドもブロックできます。公式のサンプルは次の形です。
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q "drop table"; then
echo "Blocked: dropping tables is not allowed" >&2 # stderr が Claude へのフィードバック
exit 2
fi
exit 0
ここで暴発を防ぐために知っておきたいのが if フィールドです。matcher はツール名(Bash など)でしか絞れませんが、if を使うとツールの引数まで含めて絞り込めます。例えば「Bash の中でも git コマンドのときだけ hook を走らせたい」なら、次のように書きます。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
}
]
}
]
}
}
if: "Bash(git *)" により、hook プロセスは Bash コマンドが git * にマッチするときだけ起動します。すべての Bash コマンドでスクリプトを起動するより無駄がなく、対象外のコマンドへ誤って干渉する事故も防げます(if フィールドは Claude Code v2.1.85 以降が必要です。以前のバージョンは無視され、マッチした全呼び出しで hook が走ります)。
なお、同じイベントに複数の hook を登録した場合、PreToolUse の許可判定は 最も制限的な答えが勝ちます。deny は ask を、ask は allow を上書きします。つまりログ用 hook と ブロック用 hook を並べても、ブロックが確実に優先されます。
ブロックは許可モードより強い|--dangerously-skip-permissionsでも止まる
PreToolUse のブロックには、もう一つ重要な性質があります。PreToolUse hook は許可モードのチェックよりも前に発火するため、permissionDecision: "deny"(または exit 2)を返す hook は、bypassPermissions モードや --dangerously-skip-permissions で起動していても、ツールをブロックします。
これは「ユーザーが許可モードを緩めてもバイパスできないポリシー」を強制できることを意味します。例えば「このリポジトリでは何があっても本番 DB への破壊操作は通さない」といった組織ルールを、許可設定の緩さに関係なく効かせられます。逆向きは成り立たない点も覚えておきましょう——allow を返す hook が、設定の deny ルールを上書きすることはできません。hooks は制限を厳しくはできても、許可ルールが許す範囲を超えて緩めることはできない、という非対称性があります。
ブロックの効かせ方|exit code 2とJSON出力の使い分け
ここまで exit 2 と JSON 出力の両方が出てきました。「結局どちらをいつ使うのか」を、仕様レベルで整理します。ここを曖昧にすると「ブロックが効いたり効かなかったり」の原因になります。
stdinのJSON入力(tool_name / tool_input / session_id)を読む
まず前提として、hooks は標準入力・標準出力・標準エラー出力・終了コードを通じて Claude Code とやり取りします。イベントが発火すると、Claude Code はイベント固有のデータを JSON として hook の標準入力に渡します。PreToolUse の場合、受け取る JSON は次のような形です。
{
"session_id": "abc123",
"cwd": "/Users/sarah/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}
session_id(セッションの一意 ID)と cwd(作業ディレクトリ)はすべてのイベント共通で、tool_name や tool_input のようなフィールドはイベントごとに追加されます。スクリプトはこの JSON を jq などで解析し、tool_name や tool_input.command を見て処理を分岐させます。
exit code 0 / 2 / その他の意味|exit 2とJSONの混在禁止
終了コードによる制御は、次の3つを押さえれば十分です。
終了コード | 意味 |
|---|---|
| 異議なし。アクションは通常どおり進行。 |
| ブロック。stderr に書いた理由が Claude へのフィードバックになる |
その他 | アクションは続行。トランスクリプトに |
ここで最も重要な注意点があります。公式ドキュメントは「exit 2 で stderr メッセージによりブロックするか、exit 0 で JSON により構造化制御するか、どちらか一方にせよ。混在させてはいけない」と明記しています。なぜなら、Claude Code は exit 2 のとき stdout の JSON を無視する からです。「exit 2 しつつ JSON も返す」と書くと、JSON 側の細かい制御(理由メッセージや許可判定)は読まれず、期待した挙動になりません。
判断軸はシンプルです。
- 単純にブロックしたいだけ → exit 2 + stderr に理由を書く(JSON は書かない)
- allow / deny / ask を細かく制御したい → exit 0 + stdout に JSON を返す(exit 2 にしない)
stdout JSONで permissionDecisionを返す(allowでもdenyルールは効く)
より細かい制御が必要なら、exit 0 で終了し、stdout に JSON を返します。PreToolUse では hookSpecificOutput.permissionDecision で判定を返します。
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Use rg instead of grep for better performance"
}
}
permissionDecision には3つの値があります(PreToolUse 固有)。
"allow": インタラクティブな許可プロンプトをスキップする。ただし後述のとおり deny / ask ルールは引き続き効く"deny": ツール呼び出しをキャンセルし、permissionDecisionReasonを Claude にフィードバックする"ask": 通常どおりユーザーに許可プロンプトを表示する
注意したいのは "allow" の挙動です。"allow" はインタラクティブなプロンプトをスキップするだけで、許可ルールを上書きしません。設定のどこか(管理設定を含む)に該当する deny ルールがあれば、hook が "allow" を返してもツールはブロックされます。前の章で触れた「制限は厳しくできても、緩める方向には効かない」という非対称性が、ここでも一貫しています。「allow を返したのにブロックされる」と混乱しないよう、この仕様を押さえておきましょう。
暴発させない設計|matcher絞り込み・無限ループ・自動承認の落とし穴

hooks は強力ゆえに、設定を誤ると「全ツールがブロックされる」「Claude が止まらない」といった事故を起こします。「強力すぎて逆に事故りそう」というためらいを潰すため、本番運用で先回りすべき暴発パターンと回避策をまとめます。
matcher / if で最小スコープに絞る(全発火・全ブロックを防ぐ)
最も多い事故が、matcher を空または .* にして全ツールで発火させてしまうことです。matcher を指定しないと、その hook はそのイベントのすべての発生で発火します。ブロック系の hook を全ツールにかければ何も実行できなくなり、自動承認系の hook を .* にすればファイル書き込みやシェルコマンドを含むすべての許可プロンプトを自動承認してしまいます。
回避策は「最小スコープに絞る」の一点です。
- フォーマットは
matcher: "Edit|Write"のように対象ツールを限定する - Bash コマンドを絞りたいときは
if: "Bash(git *)"のように引数まで含めて絞る - 自動承認(
PermissionRequest)の matcher はExitPlanModeのように極小に保つ(.*や空は厳禁)
「広く設定して様子を見る」のではなく、「狭く設定して必要に応じて広げる」のが鉄則です。
Stopの無限ループを防ぐ(stop_hook_activeと8回上限)
Stop hook で「タスクが終わってなければブロックして続けさせる」設計をすると、ブロックが連鎖して Claude が止まらなくなる危険があります。Claude Code には安全弁として、Stop hook が進捗なしで8回連続ブロックすると自動的にオーバーライドする仕組みがありますが、それに頼り切るのは禁物です。
正しくは、stdin の JSON に含まれる stop_hook_active フィールドを見て、すでに Stop hook が作動中なら早期に終了させます。
#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # Claude が停止することを許可
fi
# ... ここに完了チェックのロジック
これで「無限に止まらない」事故を防げます。正当に8回以上の反復が必要なケースでは、環境変数 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP で上限を引き上げられますが、まずは早期 return を入れるのが基本です。
自動承認・並列実行・タイムアウト・シェル出力混入の注意点
そのほか、本番で踏みやすい点をまとめておきます。
- 並列実行と重複排除: 同じイベントにマッチする hooks は並列で実行され、同一コマンドは自動的に重複排除されます。「1つの hook の deny が、別の hook の副作用を止める」ことは保証されないので、1 hook = 1 責務を基本にし、副作用の抑制を他 hook に依存させないようにします。また
PreToolUseで複数の hook が引数を書き換える(updatedInput)と、最後に完了したものが勝つ(順序は非決定的)ため、同じツールの入力を複数 hook で書き換えるのは避けます。 - タイムアウト:
command型 hook のタイムアウトは10分です(ただしUserPromptSubmitは30秒に短縮されます)。重い処理を hook に入れるなら、timeoutフィールドで個別に調整するか、処理自体を軽くすることを検討します。 - シェルプロファイルの出力混入: hook が JSON を返す設計なのに、
~/.bashrcや~/.zshrcに無条件のechoがあると、その出力が hook の JSON の先頭に混入してJSON 解析が失敗します。プロファイルの echo はif [[ $- == *i* ]]; then ... fiでインタラクティブシェルのときだけ実行するようにしておきましょう。
経験的にも、規約を CLAUDE.md に書くだけでは守られず、決定論的なゲート(チェックスクリプト)を併用すると安定します。その際、各 hook を「最小権限・1処理1責務」で小さく保つほど、暴発しにくく、後からデバッグもしやすくなります。
まとめ|まず入れる3つのhookと段階的な広げ方
ここまで、hooks が規約遵守を「運次第」から「保証」に変える仕組み(プロンプトは確率・hooks は決定論)から始め、効くイベントの選び方、settings.json のスコープ設計、自動フォーマットと機密ファイル保護の実装、exit 2 と JSON の使い分け、そして暴発させない設計までを見てきました。
最初から全部やる必要はありません。まずは次の3つ(必要なら4つ目)を .claude/settings.json に入れることをおすすめします。
- PostToolUse の auto-format(
matcher: "Edit|Write")— フォーマット忘れをなくす最頻出の効果 - PreToolUse の機密ファイル保護(
protect-files.sh+ exit 2)— 裏テーマの核心。「.envは必ず守る」 - **Notification の入力待ち通知 — ターミナル監視からの解放
- (必要なら)Stop の完了チェック**(
stop_hook_activeの早期 return 込み)— やり残し・取りこぼし対策
進め方の指針は明確です。チーム共通の規約は .claude/settings.json にコミットして全員に展開 し、ロジックが増えてきたら .claude/hooks/*.sh に外部スクリプト化して見通しを保つ。そして matcher / if で最小スコープに絞り、暴発させない。この順序で広げれば、「規約を守らせきれない」状態から「規約が hooks で自動的に強制される」状態へ、無理なく移行できます。
CLAUDE.md やプロンプトでの「お願い」は引き続き有効ですが、定型処理と危険操作のブロックは hooks に任せる——この役割分担ができれば、Claude Code をチームに安心して広げられます。
よくある質問(FAQ)
Q. hooks と CLAUDE.md・プロンプトはどう使い分ければよいですか?
A. 「必ず守らせたい定型処理・危険操作」は hooks、「文脈の共有や方針の伝達」は CLAUDE.md / プロンプト、と分けます。hooks は決定論的に必ず発火しますが、書けるのはルールで表現できる処理だけです。「コミットメッセージが適切か」のような曖昧な判断は CLAUDE.md やプロンプトに残すか、type: "prompt" / type: "agent" の hook を使います。
Q. ブロックは PreToolUse と PostToolUse のどちらでやるべきですか?
A. 「実行されてからでは遅い操作」(機密ファイル編集・危険コマンド)は PreToolUse で事前にブロックします。PostToolUse はツールが成功した後に発火するため、操作を取り消せません。PostToolUse のブロックは「結果に問題があったので Claude に直させる」(lint / テストの失敗フィードバック)用途に向きます。
Q. -p(ヘッドレス・非対話実行)でも hooks は効きますか?
A. PreToolUse / PostToolUse などのツール系 hooks は効きます。ただし PermissionRequest hook は非インタラクティブモード(-p)では発火しません。自動化された許可・ブロック判定が必要なら、PreToolUse を使ってください。なお -p での無人実行(cron / CI など)そのものの設計は hooks とは別軸の自動化テーマなので、ヘッドレス実行と権限設計を扱った記事も合わせて参照すると、「実行を任せる」自動化と「動作を保証する」hooks の両輪が見えてきます。
Q. hooks が発火しないときは何を確認すればよいですか?
A. 次の順で切り分けます。(1) /hooks を実行し、意図したイベントの下に hook が表示されているか。(2) matcher がツール名と正確に一致しているか(matcher は大文字小文字を区別します)。(3) スクリプトに実行権限があるか(chmod +x)。(4) jq がインストールされているか。(5) echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh のようにサンプル JSON を流し込んで、スクリプト単体が正しく動くか。詳細は claude --debug-file /tmp/claude.log でデバッグログを取ると追いやすくなります。
Q. 曖昧な判断を hook にやらせたい場合はどうすればよいですか?
A. シェルコマンド(type: "command")ではルールで書き切れない判断は、type: "prompt"(シングルターンの LLM 評価。デフォルトは Haiku)や type: "agent"(ファイル読み取りやコマンド実行を伴うマルチターン検証。実験的機能)を使います。「停止前に全タスクが完了しているか LLM に判定させる」といった用途に向きます。ただしエージェント hooks は実験的で挙動が変わる可能性があるため、本番ワークフローではまずコマンド hook を優先するのが無難です。



