Claude Code hooksレシピ7本:危険コマンド阻止・.env保護・自動整形・Stopでテスト
Claude Code hooksレシピ7本。PreToolUseでrm -rfや.envを止め、PostToolUseで整形、Stopでテストを差し戻す設定を、Windowsでの実行出力付きで紹介。導入前に決める安全境界も整理。
Claude Code の hooks は、ツール実行の前後やセッションの開始時など決まったタイミングで、指定したコマンドを必ず実行する仕組みです。 用途は「止める」「整える・確かめる」「文脈を足す」に分けると選びやすく、止めたいときは終了コード2、細かい制御は標準出力の JSON で返します。
この記事はそのレシピを7本まとめました。各レシピに設定の JSON、スクリプト、入力 JSON の要点、終了コードと出力の意味、Windows での注意を付けています。設定ファイルの置き場所と優先順位はsettings.json 逆引きにあるので、ここでは hooks を入れる前に決める権限と停止条件を短く押さえたあと、実装に絞ります。
- 止めるなら PreToolUse で
exit 2。 stderr が拒否理由として Claude に渡ります。--allowedToolsで許可済みのコマンドでも止まりました。 exit 1は止めない。 2以外は非ブロッキングのエラーで処理は進みます。スクリプトのパス間違いも同じ扱いで、ゲートが黙って外れます。- Windows はパスとツール名に注意。
file_pathは\区切りで届き、Claude はシェル操作に Bash ツールと PowerShell ツールの両方を使います。matcher はBash|PowerShellにします。 - 文脈を足すなら SessionStart と UserPromptSubmit。 この2つは、終了コード0の標準出力(JSON 以外)がそのまま Claude の文脈に入る数少ないイベントです。
hooks を入れる前に決める安全境界
hooks と MCP は、どちらもエージェントを強くすると同時に、事故の範囲も広げます。MCP は外部サービスやデータへの接続を増やし、hooks はツール実行の前後に処理を差し込むからです。公式の MCP ドキュメントは、MCP を通じて Issue tracker、monitoring dashboard、database、API などにアクセスできると説明しています。
| 観点 | MCP | hooks |
|---|---|---|
| 役割 | 外部サービスやデータへの接続を増やす | ツール実行の前後に処理を差し込む |
| 主なリスク | 読める情報・書ける対象が増える | 誤った自動処理が繰り返される |
| 最初の設計 | 読み取り専用から始める | PreToolUse と Stop で止める |
| 確認すべきもの | 認証情報、ログ、アクセス範囲 | 危険コマンド、検証未実行、機密値 |
MCP は「便利な接続」ではなく「権限の追加」です。 サーバーを足す前に、読み取り専用か書き込みありか、個人スコープかプロジェクト共有か、認証情報をどこに置くか、ログに何が残るか、失敗時に人間へどう通知するかを決めます。最初は読み取り専用で始めるのが安全で、Issue、PR、監視ログ、ドキュメントを読ませるだけでもエージェントの文脈はかなり増えます。
hooks は自動化より先にブレーキから入れます。 自動整形や通知のような便利系から入れると、止める側の設計が後回しになりがちです。この記事のレシピで言えば、危険操作を止めるレシピ1・2(PreToolUse)と、検証を済ませないまま終えるのを止めるレシピ4(Stop)を先に置き、整形(レシピ3)や通知(レシピ7)はその後に足します。止める仕組みのない自動化は、失敗も速くします。
フックで何を止めるべきかは、次の6点を README かエージェント用の指示ファイルに書き出すとはっきりします。
- 触ってよい外部サービス
- 読み取り専用のサービス
- 書き込み前に承認が必要な操作
- 表示してはいけない機密情報
- 実行してよいコマンド
- 完了報告前に必要な検証
エージェントの安全性は、モデルの性格ではなく作業環境の設計で作ります。以下のレシピは、その設計を実行時に強制するための部品です。
先に押さえる仕組み
設定は3段です。 settings.json の hooks の下に「イベント名 → matcher グループ → ハンドラ」の順で書きます。matcher は文字列で、英数字と _ - , 空白と縦棒だけなら完全一致(Edit|Write はどちらか一方)、それ以外の文字を含むと JavaScript の正規表現として部分一致で評価されます。大文字小文字は区別されます。UserPromptSubmit や Stop のように matcher を持たないイベントに matcher を書いても、黙って無視されます。
{ "hooks": { "PreToolUse": [ { "matcher": "Bash|PowerShell", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous.sh" } ] } ] }}入力は stdin に届く JSON です。 全イベント共通で session_id transcript_path cwd permission_mode hook_event_name が入り、イベントごとの項目が加わります。今回ログに残した PreToolUse の実際の入力です(ID とパスは省略)。
{ "session_id": "ba131e8d-...", "transcript_path": "C:\\Users\\...\\ba131e8d-....jsonl", "cwd": "C:\\Users\\...\\cc-hooks\\proj", "scratchpad_dir": "C:\\Users\\...\\scratchpad", "prompt_id": "aee1a741-...", "permission_mode": "default", "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": { "command": "rm -rf build-cache", "description": "Delete build-cache directory" }, "tool_use_id": "toolu_01PQWVR6..."}出力は終了コードと stdout です。 公式リファレンスの「Exit code output」を要約するとこうなります。
| 終了コード | 意味 |
|---|---|
| 0 | 成功。stdout が { で始まり } で終われば JSON として解釈する。それ以外の stdout はデバッグログへ(SessionStart と UserPromptSubmit などは Claude の文脈へ) |
| 2 | ブロック。止められるイベントなら止め、JSON の reason か stderr が理由になる。JSON で "allow" を返しても覆らない |
| それ以外(1 を含む) | 非ブロッキングのエラー。処理は続き、stderr の1行目が hook error の通知として表示される |
Windows で動くシェル。 args を書かない形(shell form)の command は、macOS / Linux では sh -c、Windows では Git Bash、Git Bash が無ければ PowerShell で実行されます。ハンドラに "shell": "powershell" を付けると個別に PowerShell にでき、pwsh.exe が優先されます。今回のスクリプトはすべて Git Bash で動かしました。jq は winget で入れたものが Git Bash の PATH から見えていました。
止めるレシピ
レシピ1: 危険なシェルコマンドを止める(PreToolUse、実行済み)
設定は上の例のとおり、PreToolUse に matcher Bash|PowerShell です。
スクリプト .claude/hooks/block-dangerous.sh:
#!/bin/bash# PreToolUse (matcher: Bash|PowerShell) : 危険なシェルコマンドを実行前に止めるINPUT=$(cat)CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty')
PATTERNS='(^|[[:space:];&|(])rm([[:space:]]+-[^[:space:]]*)*[[:space:]]+-[a-zA-Z-]*[rR]|Remove-Item[^|;]*-Recurse|git[[:space:]]+push[^|;&]*[[:space:]](--force|-f)([[:space:]]|$)|git[[:space:]]+reset[[:space:]]+--hard|drop[[:space:]]+(table|database)'
if printf '%s' "$CMD" | grep -Eiq "$PATTERNS"; then echo "Blocked by block-dangerous.sh: this command matches a forbidden pattern. Ask the user to run destructive commands themselves." >&2 exit 2fiexit 0入力の要点: tool_input.command にコマンド文字列が入ります。PowerShell ツールでも同じ command です。
終了コードと出力: パターンに当たれば stderr に理由を書いて exit 2、Claude にはそれが拒否理由として届きます。当たらなければ exit 0 で、通常の権限確認に進みます。
実行結果: --allowedTools "Bash(rm *)" で rm を明示的に許可したうえで、rm -rf build-cache の実行を頼みました。
tool_use Bash {"command":"rm -rf build-cache","description":"Delete build-cache directory"}tool_result (is_error: true)PreToolUse:Bash hook error: ["$CLAUDE_PROJECT_DIR"/.claude/hooks/block-dangerous.sh]: Blocked by block-dangerous.sh: this command matches a forbidden pattern. Ask the user to run destructive commands themselves.build-cache/keep.txt は残っていました。公式 hooks-guide によれば、PreToolUse の拒否は bypassPermissions モードや --dangerously-skip-permissions でも有効で、フックは権限ルールより厳しくはできても緩めることはできません。記事掲載にあたって rm -r -f や rm --recursive も拾うようにパターンを広げ、16通りのサンプル入力で期待どおりの終了コードになることを確かめています(rm -f foo-bar.log や git push --force-with-lease は通します)。
Windows の注意: 同じ PC で行った Skills の検証では、ファイル一覧の取得に Claude が PowerShell ツールを使った回がありました。公式も、PowerShell ツールが有効な Windows では Claude が PowerShell を主なシェルとして扱うとして、matcher を Bash|PowerShell にするよう書いています(今回の rm の検証で選ばれたのは Bash ツールでした)。パターンに Remove-Item -Recurse を入れているのはそのためです。
これはコマンド文字列の照合なので、find . -delete や別言語のスクリプトによる削除は止まりません。対象のコマンドのときだけスクリプトを起動するハンドラの if フィールド(例 "if": "Bash(rm *)")もありますが、公式はこれを best-effort と位置づけ、確実な許可・拒否には権限システムを使うよう書いています。削除系は deny ルール、到達範囲の制限はサンドボックスと組み合わせます(権限プロンプトを減らす設定)。
レシピ2: .env など機密ファイルの読み書きを止める(PreToolUse、実行済み)
設定:
{ "hooks": { "PreToolUse": [ { "matcher": "Read|Edit|Write", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-secrets.sh" } ] } ] }}スクリプト .claude/hooks/protect-secrets.sh:
#!/bin/bash# PreToolUse (matcher: Read|Edit|Write) : 機密ファイルの読み書きを止めるINPUT=$(cat)FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')FILE_PATH="${FILE_PATH//\\//}" # Windows は \ 区切りで届くので / にそろえるBASE="${FILE_PATH##*/}"
case "$BASE" in .env.example|.env.sample) exit 0 ;; .env|.env.*|*.pem|*.key|id_rsa|id_ed25519) echo "Blocked by protect-secrets.sh: $BASE is a secret file. Ask the user for the non-secret value you need." >&2 exit 2 ;;esaccase "$FILE_PATH" in */.git/*|*/secrets/*) echo "Blocked by protect-secrets.sh: $FILE_PATH is a protected path." >&2 exit 2 ;;esacexit 0入力の要点: tool_input.file_path は常に絶対パスで、~ や相対パスは展開済みです。Windows ではバックスラッシュ区切りで届きます。公式リファレンスのとおり、比較の前に / へそろえないと */.git/* のようなパターンは一致せず、黙って通ります。
終了コードと出力: 機密ファイルなら exit 2。.env.example は先に exit 0 で通します。
実行結果: Read ツールで .env を読むよう頼みました。
tool_use Read {"file_path":"C:\\Users\\...\\proj\\.env"}tool_result (is_error: true)PreToolUse:Read hook error: ["$CLAUDE_PROJECT_DIR"/.claude/hooks/protect-secrets.sh]: Blocked by protect-secrets.sh: .env is a secret file. Ask the user for the non-secret value you need.debug ログ側では、ファイル名が [REDACTED] is a secret file と伏せ字で記録されていました。サンプル入力でも C:\proj\.git\config と server.pem は2、.env.example と src\index.ts は0を返しています。
Windows の注意: パスをそろえる5行目が要です。この1行が無いと、Windows ではディレクトリ単位の保護が効きません。
このレシピで塞げないものもあります。公式によれば、PreToolUse はツール呼び出しにしか発火せず、プロンプトで @ を付けて参照したファイルはツールを経由せずに取り込まれます。それを塞ぐのは Read の deny ルールです。Bash や PowerShell で cat .env とする経路も、この matcher では見ていません。settings の permissions.deny との併用を前提にしてください(書式はsettings.json 逆引き、考え方はAIエージェントに秘密情報を読ませない)。
整える・確かめるレシピ
レシピ3: 編集後に prettier で整形する(PostToolUse、実行済み)
設定:
{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format-after-edit.sh" } ] } ] }}スクリプト .claude/hooks/format-after-edit.sh:
#!/bin/bash# PostToolUse (matcher: Edit|Write) : 編集されたファイルを prettier で整形するINPUT=$(cat)FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')[ -z "$FILE_PATH" ] && exit 0
case "$FILE_PATH" in *.js|*.jsx|*.ts|*.tsx|*.json|*.css|*.md) ;; *) exit 0 ;;esac
PRETTIER="${PRETTIER_BIN:-$CLAUDE_PROJECT_DIR/node_modules/prettier/bin/prettier.cjs}"[ -f "$PRETTIER" ] || exit 0
if ! OUT=$(node "$PRETTIER" --write "$FILE_PATH" 2>&1); then # 整形に失敗した(=構文エラーの可能性)ときだけ Claude に知らせる printf 'prettier failed on %s:\n%s\n' "$FILE_PATH" "$(printf '%s' "$OUT" | head -n 5)" >&2 exit 2fiexit 0公式 hooks-guide の1行版 jq -r '.tool_input.file_path' | xargs npx prettier --write をスクリプトにしたものです。拡張子で対象を絞り、npx を介さずプロジェクトの prettier 本体を node で起動しています。公式リファレンスにも、Windows で args を使う exec form の場合は node_modules/.bin の .cmd シムを起動できないので、node とスクリプトのパスで呼ぶよう書かれています。検証では prettier 3.9.6 の場所を settings の env で PRETTIER_BIN として渡しました。
入力の要点: tool_input.file_path と、ツールの結果の tool_response が届きます。
終了コードと出力: 整形できれば exit 0 で、画面には何も出ません。prettier が失敗したときだけ stderr に先頭5行を書いて exit 2 にしています。PostToolUse の exit 2 はツールの実行を取り消せませんが、stderr が Claude に表示されるので、壊れたファイルを直させる合図になります。exit 0 のときの stderr は Claude に届かない、と公式に明記されています。
実行結果: Write ツールで1行の JSON を書かせました。
Claude が書いた内容: {"name":"demo","tags":["a","b"],"nested":{"x":1}}フック後のファイル: { "name": "demo", "tags": ["a", "b"], "nested": { "x": 1 } }
[INFO] PostToolUse hook modified ...\proj\src\config.json after Write — re-synced readFileStatedebug ログには、フックによる書き換えを検知して、Claude Code が保持しているファイルの状態を同期し直した記録が残っていました。
Windows の注意: バックスラッシュ区切りの file_path はそのまま node に渡して整形できたので、このレシピではパスを変換していません。なお公式によれば、Bash コマンドや Claude Code の外のプロセスが書き換えたファイルには、Edit|Write に一致するこのフックは発火しません。どう変わっても整形したい特定のファイルは FileChanged イベントで扱います。
レシピ4: Stop でテストを走らせ、失敗なら続けさせる(Stop、実行済み)
設定:
{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run-tests-on-stop.sh" } ] } ] }}スクリプト .claude/hooks/run-tests-on-stop.sh:
#!/bin/bash# Stop : 応答を終える前にテストを走らせ、失敗なら続行させるINPUT=$(cat)# Stop フックで一度続行させた後なら止める(無限ループ防止)if [ "$(printf '%s' "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then exit 0fi
cd "$CLAUDE_PROJECT_DIR" || exit 0if ! OUT=$(node test.js 2>&1); then printf 'Tests failed (node test.js). Fix the code before finishing.\n%s\n' "$(printf '%s' "$OUT" | head -n 8)" >&2 exit 2fiexit 0入力の要点: stop_hook_active は、Stop フックによる続行の最中なら true になります。last_assistant_message には直前の応答本文が入ります。
終了コードと出力: テストが落ちたら、stderr にテスト出力の先頭を書いて exit 2。Claude は止まらず、stderr を「続けるべき理由」として受け取ります。公式は、解決しない条件で延々とブロックしないよう stop_hook_active を見て抜けることを求めており、8回連続でブロックすると Claude Code が打ち切ります(CLAUDE_CODE_STOP_HOOK_BLOCK_CAP で変更可)。
実行結果: sum.js に a - b のバグを仕込み、「OK とだけ答えて」と頼みました(抜粋)。
assistant: OKuser (Stop hook feedback): ["$CLAUDE_PROJECT_DIR"/.claude/hooks/run-tests-on-stop.sh]: Tests failed (node test.js). Fix the code before finishing. AssertionError [ERR_ASSERTION]: sum(2, 3) should be 5 -1 !== 5assistant: Read test.js → Read sum.js → Edit (a - b を a + b に) → Bash: node test.js → okresult: Fixed. The sum() function was using subtraction instead of addition. ... (num_turns: 7)フックに届いた入力は、1回目が "stop_hook_active": false と "last_assistant_message": "OK"、修正後の2回目が "stop_hook_active": true でした。
Windows の注意: テストの出力は CRLF 改行のまま Claude に渡りましたが、読み取りに支障はありませんでした。Stop はタスク完了時だけでなく応答を終えるたびに発火するので、時間のかかるテストは対象を絞ります。
文脈を足す・知らせるレシピ
レシピ5: 時刻を足し、キー入りのプロンプトを止める(UserPromptSubmit、実行済み)
設定:
{ "hooks": { "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/prompt-guard.sh" } ] } ] }}スクリプト .claude/hooks/prompt-guard.sh:
#!/bin/bash# UserPromptSubmit : 秘密情報らしき文字列を含むプロンプトを止め、そうでなければ文脈を足すINPUT=$(cat)PROMPT=$(printf '%s' "$INPUT" | jq -r '.prompt // empty')
if printf '%s' "$PROMPT" | grep -Eq '(sk-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]{30,}|AKIA[0-9A-Z]{16})'; then jq -n '{decision: "block", reason: "Prompt looks like it contains an API key. Remove it and send again.", hookSpecificOutput: {hookEventName: "UserPromptSubmit", suppressOriginalPrompt: true}}' exit 0fi
NOW=$(date '+%Y-%m-%d %H:%M %z')jq -n --arg now "$NOW" '{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext: ("Local time when the prompt was sent: " + $now)}}'入力の要点: prompt に送信したテキストが入ります。
終了コードと出力: どちらの分岐も exit 0 と JSON です。止めるときはトップレベルの decision: "block" と reason(ユーザーに表示され、Claude の文脈には入らない)。文脈を足すときは hookSpecificOutput.additionalContext で、Claude にはシステムリマインダーとして届き、チャット欄には表示されません。既定のタイムアウトは、多くのイベントの600秒ではなく30秒です。超えると追加の文脈は捨てられ、プロンプトだけが Claude に届きます。
実行結果: 通常のプロンプトでは、debug ログに Hook UserPromptSubmit (...prompt-guard.sh) provided additionalContext (59 chars) と出て、Claude は「the prompt was sent at 2026-09-14 21:40 +0900」と答えました。ダミーのキーを含めると、モデルを呼ばずに終わりました(num_turns: 0、コスト0)。
UserPromptSubmit operation blocked by hook:Prompt looks like it contains an API key. Remove it and send again.
Original prompt: Deploy to production with key sk-test000000000000000000000000この出力は suppressOriginalPrompt を付ける前のもので、止めたメッセージにキーがそのまま表示されています。 公式の UserPromptSubmit の表には、元のプロンプトを表示から外す suppressOriginalPrompt があります。最初にトップレベルへ置いたときは、debug ログに Hook JSON output had unrecognized keys (ignored): suppressOriginalPrompt. と出て効きませんでした。上のスクリプトのように hookSpecificOutput の中へ置き直すと、表示は UserPromptSubmit operation blocked by hook: と理由の2行だけになり、Original prompt: の行が消えました。
Windows の注意: Windows 版の jq は JSON を CRLF 改行で出力しましたが、Successfully parsed and validated hook JSON output と正常に解釈されました。一方、公式 hooks-guide によれば、Git Bash はシェルのプロファイルを読むことがあり、そこに無条件の echo があると JSON の前に文字列が混ざって、JSON として扱われなくなります。
レシピ6: 作業メモと git の状態を読み込む(SessionStart、実行済み)
設定:
{ "hooks": { "SessionStart": [ { "matcher": "startup|resume|clear|compact", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-context.sh" } ] } ] }}スクリプト .claude/hooks/session-context.sh:
#!/bin/bash# SessionStart : 作業メモと git の状態を Claude の文脈に読み込む(stdout がそのまま文脈になる)cd "$CLAUDE_PROJECT_DIR" || exit 0echo "Project state loaded by SessionStart hook:"if [ -f NOTES.md ]; then echo "--- NOTES.md ---" head -n 20 NOTES.mdfiif git rev-parse --is-inside-work-tree >/dev/null 2>&1; then echo "--- git ---" echo "branch: $(git branch --show-current)" git status --short | head -n 20fiexit 0入力の要点: source が startup / resume / clear / compact / fork のどれかで、matcher はこの値に当たります。compact を含めておくと、コンパクションで会話が要約された後にも読み直されます。
終了コードと出力: SessionStart は止められないイベントで、exit 0 の標準出力(JSON 以外)がそのまま Claude の文脈に入ります。sessionTitle などを一緒に返したいときだけ JSON にします。使えるハンドラは command と mcp_tool だけで、毎セッション走るので速く保ちます。後続の Bash コマンドに環境変数を残したいときは、CLAUDE_ENV_FILE が指すファイルに export 行を追記します。
実行結果: ツールを使わずに NOTES.md の内容を答えるよう頼むと、「Current task is fixing the sum() bug」と返りました。stream-json には次のイベントが記録されていました。
{"subtype":"hook_response","hook_name":"SessionStart:startup","hook_event":"SessionStart","exit_code":0,"outcome":"success","stdout":"Project state loaded by SessionStart hook:\n--- NOTES.md ---\n# NOTES\n- 現在の作業: sum() のバグ修正\n- テストは node test.js\n"}公式は、スクリプトの要らない固定の情報は CLAUDE.md に書くよう勧めています。SessionStart に向くのは、ブランチや未コミットの変更、担当中の Issue のように毎回変わる状態です。
Windows の注意: 検証ディレクトリは git 管理外だったので、git の部分は git rev-parse の判定で飛ばされています。git が PATH に無い環境でも同じく飛ばされます。
レシピ7: 入力待ちを通知する(Notification、公式仕様ベース、未実行)
Notification は、Claude Code が許可や入力を待っているときなどに発火し、止めることはできません。matcher は permission_prompt(許可の確認が約6秒続いたとき)や idle_prompt(応答後、約60秒入力が無いとき)などです。公式 hooks-guide が Windows 向けに載せているのは、PowerShell でダイアログを出す設定です。
{ "hooks": { "Notification": [ { "matcher": "", "hooks": [ { "type": "command", "command": "powershell.exe -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention', 'Claude Code')\"" } ] } ] }}入力の要点: message、任意の title、どの種類かを示す notification_type が届きます。
終了コードと出力: 終了コードと stderr は無視され、systemMessage や continue も捨てられます。例外は terminalSequence で、JSON でエスケープシーケンスを返すと Claude Code がターミナルへ書き出します。公式リファレンスでは、Windows Terminal の通知は OSC 9 に対応すると書かれています。ただし -p の非対話実行では無視されます。
Windows の注意: 公式の注記どおり、このダイアログはターミナルの裏に開くことがあります。先に PowerShell で単体実行して表示を確かめてから登録します。
動かないときの確認
/hooksで登録を見る。 イベントごとの件数、matcher、定義元(User Settings / Project Settings / Local Settings / Plugin Hooks など)が表示されます。読み取り専用なので、変更は設定ファイル側で行います。設定ファイルの編集は通常ファイル監視で自動反映され、反映されなければ再起動します。claude --debug-file <path>で起動してログを見る。 今回もHook output does not start with {, treating as plain textやHook JSON output had unrecognized keys (ignored)の行が、そのまま原因を示していました。--debugだけの場合は~/.claude/debug/<session-id>.txtに書かれ、ターミナルには出ません。- スクリプトに JSON を手で流す。 検証中、エスケープの崩れた手書き JSON で jq がパースエラーを出したので、
jq -n --argで組み立て直しました。Git Bash では次のように確かめられます。
jq -nc --arg p 'C:\proj\.env' '{tool_name:"Read",tool_input:{file_path:$p}}' | ./.claude/hooks/protect-secrets.sh; echo "exit=$?"exit 1とパス間違いを疑う。 どちらも非ブロッキングのエラーで、Failed with non-blocking status code:の通知が出るだけで処理は進みます。ポリシー用のフックは、初回に必ずブロックされることを確認します。- JSON が効かないなら位置を疑う。
permissionDecisionやadditionalContextをトップレベルに置くと、エラーにならずに無視されます。 - 他人のリポジトリで
-pを回す前に.claude/を読む。 command フックはあなたのユーザー権限でそのまま動きます。公式によれば、-pや SDK の実行ではワークスペース信頼のダイアログが出ず、リポジトリにコミットされた.claude/settings.jsonのフックがそのまま走ります。今回の検証も、まさにその挙動で発火しています。1回だけ止めるなら--settings '{"disableAllHooks": true}'です。
Skill の frontmatter にもフックを書けます。その場合は Skill が呼ばれた時点で登録され、セッションの残りの間動きます。詳しくはClaude Code Skills の作り方にまとめました。
まとめ
- 入れる前に境界を決める。MCP は読み取り専用から、hooks は整形や通知より先にブレーキ(PreToolUse と Stop)から
- 止めるなら PreToolUse で
exit 2。exit 1やスクリプトのパス間違いは素通りする - Windows では
file_pathを/にそろえ、シェル系の matcher はBash|PowerShell - 整形は PostToolUse、失敗したときだけ
exit 2で Claude に知らせる。コマンドでの書き換えは FileChanged - Stop でのテストは
stop_hook_activeで抜ける。連続8回のブロックで打ち切られる - 文脈は SessionStart の標準出力と UserPromptSubmit の
additionalContext。固定の規約は CLAUDE.md - フックは文字列照合によるブレーキ。deny ルールとサンドボックスを併用する
claude -p を CI に組み込むときの権限の絞り方はヘッドレス実行の記事、Windows で Git Bash と PowerShell のどちらが使われるかの詳細はWindows ネイティブのトラブルを参照してください。
この記事の情報・検証メモ
- claude-code
- workflow
- security
- hooks
- mcp
- agent-ops
- 公開日
- 情報確認
- 参考リンク
- 4件
- 更新性
- 定期更新
仕様・料金・提供範囲が変わりやすいテーマは、公開日・更新日・情報確認日を分けて管理します。 導入前には必ず記事末尾の一次情報と公式ドキュメントで最新状況を確認してください。
一次情報・参考リンク
- Claude Code docs: Hooks reference https://code.claude.com/docs/en/hooks
- Claude Code docs: Automate actions with hooks https://code.claude.com/docs/en/hooks-guide
- Claude Code docs: Extend Claude Code (Hook vs Skill) https://code.claude.com/docs/en/features-overview
- Claude Code docs: Connect Claude Code to tools via MCP https://code.claude.com/docs/en/mcp