結論:hooks で止まるのは「操作」だけ

Claude Code の hooks は、ツール実行の前後などに必ず起動する仕組みです。CLAUDE.md に書く禁止事項がAIへのお願いであるのに対し、hooks はAIの判断や読み落としに関係なく動きます。だから「確実に効かせたいこと」は hooks 側に置きます。

ここは数え方が変わった箇所なので先に書きます。2026-09-24 時点の公式リファレンスでは、hook ハンドラーは command / http / mcp_tool / prompt / agent の5タイプに分かれています。 このうち prompt はプロンプトと hook の入力をモデルへ送って yes/no を返させるもの、agent はファイルを読んだり検索したりできる subagent を起動するものです(公式は agent を実験的と明記)。つまり「AIの判断に関係なく動く」のは command / http / mcp_tool の3タイプで、prompt と agent は判定そのものをモデルに委ねます。この記事で扱うのはすべて command タイプです。機械的に確実に止めたい検査は、このどちらなのかを先に決めてください。

そのうえで、止められるのは画面上でツール呼び出しとして現れる操作だけです。「雑な実装をしない」「勝手に判断しない」は hooks では止まりません。ここを取り違えると、パターン表だけが育って誰も読まなくなります。

hooksで止めるべきかの判断フロー。止めたいことがツール実行として現れるかを最初に問い、現れないなら権限設定かCLAUDE.mdで扱う。現れる場合、起きたら取り返しがつかないならPreToolUseでexit 2のブロック、取り返しがつくならPostToolUseやStopで通知にとどめる。判断の質を上げたい場合はhooksでは解決しない

この記事は仕様の解説ではありません。4ヶ月運用して何が壊れたかの記録です。結論から書くと、踏んだ罠は3つでした。

  1. コマンド文字列だけを見るガードは、外部スクリプトを1枚かませば無効になる
  2. hook を個別に登録すると、Bash 1回ごとに874msのオーバーヘッドが乗る
  3. 閾値を他人の記事から借りると、鳴りすぎるか鳴らないかのどちらかになる

この記事の数字は何を測ったものか

先に集計条件を開示します。当メディアの運営で使っているAIエージェントの作業ログ(analytics/events.sqlite)を、2026年9月2日に再集計したものです。

項目 値
集計期間 2026-05-06 〜 2026-09-01(約4ヶ月)
セッション数 745(ツール呼び出しを含むもの)
ツール呼び出し 24,773件
環境 Windows 11 / Python 3.11.1 / Claude Code 2.1.161

内訳を出したのは、hook をどのイベントに置くと費用対効果が高いかを決めるためです。

作業ログ24,773件のツール呼び出し内訳

ツール呼び出しの 79.8% はシェル実行だった

2026-05-06〜09-01 / 745セッション・24,773件の自作作業ログを集計

だから PreToolUse × Bash に1本入れるだけで、作業の約8割が検問所を通る。

逆に、この1本を重くすると全体が遅くなる(実測874msの話は本文へ)。

シェル実行(Bash / exec) 79.8%(19,775件)
ファイル編集(apply_patch / Edit / Write) 16.2%(4,005件)
読み取り・検索(Read / Grep / Glob) 1.8%(435件)
Web・その他(WebSearch / MCP ほか) 2.2%(558件)

シェル実行が 79.8%(19,775件) でした。つまり PreToolUse × Bash に検査を1本置くだけで、作業の約8割が検問所を通ります。逆に言えば、この1本を重くすると全体が遅くなります。3章目の874msはこれが原因でした。

自分の環境で同じ集計をするなら、ログの持ち方によりますが、考え方は「どのツールが何%を占めるか」を数えるだけです。8割を占めるツールが分かれば、そこに1本置くのが最短です。

実際に動かしている12検査の配置

現在の構成です。イベントごとにディスパッチャを1本だけ登録し、その中で検査を順に回します。

実運用しているhooksの配置図。5イベントごとにディスパッチャを1本登録し、中で12検査を評価する。PreToolUse(Bash)にはアップロードガード・git・build・watchの4検査、PostToolUseに回路ブレーカーとルーティング記録、Stopに検証リマインダなど3種、UserPromptSubmitにカウンタリセット、SessionStartに品質基準の注入を配置している

イベント 置いているもの 挙動
PreToolUse (Bash) アップロード・削除ガード / git / build / watch ブロック(exit 2)と注意喚起
PostToolUse 回路ブレーカー / ルーティング記録 通知・記録のみ
Stop 検証リマインダ 3種 通知のみ
UserPromptSubmit ブレーカーのカウンタリセット 記録のみ
SessionStart 品質基準の注入 記録のみ

ブロックしているのは PreToolUse だけです。残りは通知と記録に留めています。止める対象を増やすと、止まってはいけない場面で止まって作業が進まなくなるためです。

検査の並び順にも意味があります。ディスパッチャは1つでも exit 2 を返した時点で残りを評価しません。だから事故コストが大きい検査を先頭に置きます。当メディアはアップロード・削除ガードを1番目に置いています。

外部スクリプト1枚でガードはすり抜けられた

ここが一番の失敗です。

アップロードガードの最初の実装は、コマンド文字列を正規表現で照合するだけでした。lftp put や scp user@host: を検出してブロックします。一見ちゃんと動きます。

しかし、これは通ります。

python scripts/publish.py

コマンド文字列に ftp も upload も出てきません。publish.py の中身がアップロード関数を呼んでいても、hook からは見えません。スクリプトを1枚かませるだけで、ガードは無効になっていました。

外部スクリプト経由でガードがすり抜けられた失敗と対策の比較図

外部スクリプト1枚でガードは無効になった

コマンド文字列しか見ていなかったのが原因

判定を「import の有無」にしなかった理由

対策前 対策後
python scripts/publish.py python scripts/publish.py
コマンド文字列に ftp / upload が無い .py のパスを抽出して中身を読む
→ 素通り。中身は upload_dir() を呼んでいた → upload_dir() を検出してブロック
import で判定すると ダウンロード専用スクリプトも止まる(DL も同じモジュールを import する)
関数名で判定すると upload_dir / delete_dir / storbinary など、実際に書き込む呼び出しだけ拾える
中身が読めなかったら 安全側に倒してブロックする(読めないものを通さない)

対策は、コマンドから .py のパスを抜き出して中身まで読むことです。ここで一度失敗しています。最初は「アップロード用モジュールを import していたらブロック」にしたのですが、ダウンロード専用スクリプトまで止まりました。取得系も同じモジュールを import するからです。

判定を import ではなく実際に書き込む関数名に変えて解決しました。読めなかった場合は安全側に倒してブロックします。

# 名前に ftp / upload / deploy / sync を含む .py の実行は「要確認」の印。
# 上りか下りかは名前では判別できないので、実際の判定は中身で行う。
PY_FILENAME_RE = re.compile(
    r"\bpython\b(?:\s+-\w+)*\s+\S*(?:ftp|upload|deploy|sync|publish)\S*\.py\b",
    re.IGNORECASE,
)


def scan_py_files_in_cmd(cmd):
    """python ... script.py の中身を読み、書き込み / 削除があるか判定する。

    戻り値: (checked, write_found, unreadable)
    """
    py_paths = re.findall(
        r"\bpython\b(?:\s+-\w+)*\s+[\"']?([^\s\"']+\.py)[\"']?", cmd, re.IGNORECASE
    )
    checked = write_found = unreadable = False
    for path in py_paths:
        try:
            with open(path, "r", encoding="utf-8", errors="ignore") as f:
                content = f.read(20000)
        except Exception:
            unreadable = True          # 読めなかったものは通さない
            continue
        checked = True
        if PY_WRITE_RE.search(content):
            write_found = True
    return checked, write_found, unreadable

この対策には、まだ穴があります

この記事を書いている最中に、自分のガードが自分の図解生成スクリプトをブロックしました。

上の図を描く Python スクリプトには、説明のために書き込み関数の名前が文字列として入っています。ガードは中身を読んで関数名を見つけ、アップロード処理だと判定しました。実際には SVG をローカルに保存するだけのスクリプトです。

中身の文字列照合では、「その関数を呼んでいる」のか「その関数名について書いている」のかを区別できません。 抽象構文木を解析すれば区別できますが、そこまでやるかは判断です。当メディアは今のところ、誤検知したら止まる側で運用しています。止まりすぎる分には事故になりませんが、止まる回数が増えると承認が形骸化するので、そこが次の課題です。

この種のガードは「完全に防ぐもの」ではなく「素通りを減らすもの」として設計してください。

閾値は自分のログから決めた

もう1つ運用しているのが回路ブレーカーです。1つの指示に対してツールを何回使ったかを数え、多すぎるときに手を止めるよう促します。

「30ターンで方針確認」といったルールを文章で書いても実行されません。当メディアでは、必読と決めたファイルが3ヶ月消えたまま誰も気づかなかったことがあります。文章で課したルールは実行されないという前提で、機械的に数えることにしました。

問題は閾値です。他人の記事の数字を借りても効きません。 借りると、鳴りすぎて無視するか、鳴らなくて意味がないかのどちらかになります。自分の作業ログ415ブロック(人間の指示1件を1ブロックとして集計)の分布を見て決めました。

1指示あたりのツール呼び出し数の分布と回路ブレーカーの閾値。自作ログ415ブロックの集計で中央値4回、75パーセンタイル12回、90パーセンタイル24回、95パーセンタイル34回、最大149回。上位9%にあたる25回で方針確認の警告、上位2%にあたる60回で分割を促す設計

統計量 ツール呼び出し数
中央値 4
75パーセンタイル 12
90パーセンタイル 24
95パーセンタイル 34
最大 149

上位9%にあたる 25回で方針確認の警告、上位2%にあたる 60回で分割を促す設計にしました。同じ段階では二度鳴らしません。

正直に書くと、この閾値が妥当かはまだ分かりません。 導入後に記録できたのは28ブロックで、警告が2回、分割の促しが2回、最大は76回です。n が小さく、閾値の検証には足りていません。3ヶ月後に見直します。

自分の環境で同じことをするなら、ブロックごとのツール呼び出し数を並べて、パーセンタイルを取るだけです。中央値ではなく上位側で切るのがコツで、中央値付近で切ると通常作業のたびに鳴ります。

hooks が動かない・遅いときの診断

設定したのに動かない、という状態には決まった原因があります。上から順に見てください。

症状 最初に見るところ
何も起きない matcher の大文字小文字。公式は「マッチャーは大文字小文字を区別する」と明記。bash ではなく Bash
何も起きない matcher に書いた文字種(下記)
何も起きない settings.json の末尾カンマ。JSONとして壊れていると黙って無視される
全ツールが止まる hookスクリプトの構文エラー。シェルが exit 2 を返しブロック扱いになる
ときどき動かない スクリプト実体がクラウドドライブ上にある。未マウント時に黙って死ぬ
体感が遅い 1イベントに別々の hook を複数登録している(同一ハンドラーは公式側で重複排除される。下記)
Stop hook が途中で効かなくなる 連続8回ブロックすると Claude Code 側が上書きする。stop_hook_active を見て早期 exit する
文字化けする Windows。sys.stderr.reconfigure(encoding="utf-8") を入れる

matcher の評価方法は、書いた文字種で分岐します(公式リファレンス、2026-09-24 確認)。文字・数字・_・-・スペース・,・| だけなら完全一致(または | , 区切りの完全一致リスト)ですが、それ以外の文字を含むとアンカー無しの JavaScript 正規表現として評価されます。Edit.* は Edit と NotebookEdit の両方にマッチするので、完全一致させたいなら ^Edit$ のようにアンカーを付けます。完全一致セット内のハイフンは Claude Code v2.1.195 以降が必要で、それ以前は code-reviewer が senior-code-reviewer でも発火します。

「ときどき動かない」と「体感が遅い」は、当メディアが実際に踏んだものです。

hook をイベントごとに4本登録していた時期があり、Bash 1回ごとに Python が4プロセス起動していました。しかもスクリプト実体をクラウドドライブに置いていたため、全 Bash 呼び出しに874msのオーバーヘッドが乗っていました(2026-08-30 実測)。さらにドライブが未マウントのときは、全 hook がエラーも出さずに停止していました。

ここも数え方に注意が要ります。 公式リファレンス(2026-09-24 確認)では、マッチしたフックは並列で実行され、同一のハンドラーは自動的に重複排除されます。コマンドフックはコマンド文字列と args、HTTPフックは URL で重複排除されます。つまり「登録本数がそのまま直列の待ち時間になる」わけではなく、同じハンドラーを重複して登録しても起動は1回です。それでも別々のスクリプトを4本登録すれば4プロセスが起動することは変わりません。1プロセスあたりの起動コストが重い環境では、並列でも最も遅い1本ぶんが全 Bash 呼び出しに乗ります。当メディアの874msは、クラウドドライブ上の Python 起動が4本同時に走ったケースでした。なお非同期フック(async)は例外で、公式は「各実行は個別のバックグラウンドプロセスを作成し、同じ非同期フックの複数の発火をまたぐ重複排除は無い」と明記しています。

時間側の既定値も押さえておきます。公式のタイムアウトは command / http / mcp_tool が600秒で、UserPromptSubmit・PreModelSwitch・PostModelSwitch では30秒、MessageDisplay では10秒に短縮されます。prompt は30秒、agent は60秒(かつ最大50ツール使用ターン)、SessionEnd は全タイプ合計で1.5秒の予算を共有し、hook 側の設定が長ければ最大60秒まで引き上げられます。874msは command の600秒に対しては誤差ですが、全 Bash 呼び出しに毎回乗るので体感には出ます。

hookを個別登録した場合とディスパッチャにまとめた場合の比較図

hook を4本登録したら、Bash 1回ごとに 874ms 乗った

2026-08-30 実測。判定ロジックは変えず、起動の仕方だけ直した

判定ロジックは1行も変えていない。遅かったのは検査の中身ではなく、プロセスの起動回数だった。

hook が「重い」と感じたら、まず何プロセス起動しているかを数える。

個別登録(対策前) ディスパッチャ(対策後)
Bash 実行 1回 Bash 実行 1回
python python
4プロセス起動 / 実測 874ms 中で4検査を順に評価
ドライブ未マウント時は全部が黙って死ぬ 1つが例外を投げても他は動かす

直したのは起動の仕方だけで、判定ロジックは1行も変えていません。イベントごとにディスパッチャを1本登録し、その中で検査を順に評価します。規約は3つだけです。

  • どれか1つでも exit 2 を返したら、その時点で停止して 2 を返す(ブロック最優先)
  • exit 0 の検査の標準出力は結合して出す
  • 1つの検査が例外を投げても他は動かす(hook全体を落とさない)

hook が「重い」と感じたら、まず何プロセス起動しているかを数えてください。 検査の中身を最適化する前に、そこで解決することがあります。

何から入れるか

全部入れる必要はありません。当メディアの構成も、事故や不便を1つずつ潰した結果としてこの形になっただけです。

状況 最初に入れるもの
本番・STGへデプロイする権限をAIに渡している アップロード・削除ガード(PreToolUse / Bash)
削除・DB操作を任せている 同上(パターン表に DROP と rm -rf を追加)
長時間の自律実行をさせている 回路ブレーカー(PostToolUse)
hookを既に3本以上登録している ディスパッチャ化
どれも当てはまらない 入れない

最後の行が本音です。起きていない事故を先回りしてパターン表に足すと、表だけ育って誰も読まなくなります。 踏んだ罠を1つずつ足すのが、結果として続いた運用でした。

当メディアのアップロードガードも、先回りして作ったものではありません。確認を取らずにアップロードした事故が実際にあり、その再発防止として実装したものです。

この記事で使っている hooks 一式を、そのままコピーできる形で配布しています。ディスパッチャの実装、アップロード・削除ガード(外部スクリプト経由のすり抜け対策込み)、回路ブレーカーとカウンタリセット、settings.json の登録例、動かないときの診断表をまとめたものです。顧客名・実案件・認証情報・社内パスは含みません。ライセンスは自由で、帰属表示も不要です。

hooks スターターキットをダウンロード

hooks で止められるのは操作だけで、判断の質は上がりません。同じリポジトリを2体のAIエージェントに触らせたときの役割分担とロック設計はClaude Code と Codex の併用記事、subagent を使ったレビュー体制はコードレビュー自動化の記事、契約プランの選び方はClaude Code の料金記事で扱っています。

訂正履歴

  • 2026-09-24: 公式ドキュメント(code.claude.com/docs/ja/hooks・/hooks-guide)を再取得して照合し、仕様の数え方が変わっていた箇所を書き換えました。①hook ハンドラーが command / http / mcp_tool / prompt / agent の5タイプに分かれ、prompt と agent は判定をモデルに委ねるため、旧記述の「hooks はAIの判断に関係なく動く」を3タイプに限定する形へ修正。②「マッチしたフックは並列で実行され、同一ハンドラーは自動で重複排除される」という公式記述を追記し、登録本数が直列の待ち時間になるかのように読めた箇所を修正。③公式のタイムアウト既定値(command / http / mcp_tool 600秒、UserPromptSubmit 等30秒、MessageDisplay 10秒、prompt 30秒、agent 60秒かつ最大50ツール使用ターン、SessionEnd は合計1.5秒予算)を追記。④Stop hook は連続8回ブロックで Claude Code 側が上書きするという上限を診断表へ追加。⑤matcher の評価方法が文字種で完全一致とアンカー無し正規表現に分岐する点を追記(旧記述は「完全一致」と一律に書いていました)。自社実測値(24,773件の内訳、874ms、415ブロックの分布と最大149回、閾値25回 / 60回)は当メディアのログから出した数字で公式仕様の変更対象ではないため、変更していません。
  • 2026-09-02: 全面改稿。従来は最小構成の手順記事(サンドボックスでの実行検証)でしたが、4ヶ月の運用記録として再構成しました。追加したのは、ツール呼び出し24,773件の内訳、実運用12検査の配置、外部スクリプト経由でガードがすり抜けられた事例と対策コード、874msのオーバーヘッドとディスパッチャ化、回路ブレーカーの閾値の決め方、動かないときの診断表です。事実の訂正はありません(旧記事の実行検証結果は2026-08-11時点のもので、内容に誤りは見つかりませんでした)。旧記事にあった guard.py の最小構成は、すり抜け対策を加えた形で配布物へ移しています。
  • 2026-08-25: 前回更新。
VERIFIED
動作確認日: 2026.09.02 / 環境: Claude Code 2.1.161 / Windows 11 / Python 3.11.1 / ステータス: passed

よくある質問

hooksを設定したのに動きません。何から見ればよいですか。
最初にmatcherの大文字小文字を見てください。ツール名は完全一致で、bashではなくBashです。次にsettings.jsonが壊れていないか(末尾カンマ)を確認します。JSONとして不正だと、エラーも出ずに黙って無視されます。逆に全ツールが止まる場合は、hookスクリプト側の構文エラーを疑ってください。シェルがexit 2を返し、ブロック扱いになります。
hooksがあれば、AIに何を任せても安全ですか。
安全になりません。正規表現ベースのガードはパターン表に無い書き方を素通りしますし、当メディアは外部スクリプトを1枚かませるだけでガードがすり抜けられた事例を実際に踏んでいます。権限設定とバックアップを併用した多層防御の一層として使ってください。また、hooksが止められるのは「やってはいけない操作」であって「やる必要がなかった作業」ではありません。
hookは何本くらい登録すべきですか。
登録するのはイベントごとに1本にして、その中で複数の検査を回してください。当メディアは1イベントに4本登録していた時期があり、Bash1回ごとにPythonが4プロセス起動して実測874msのオーバーヘッドが乗っていました。検査の中身ではなく、プロセスの起動回数が原因でした。なお2026-09-24時点の公式リファレンスでは、マッチしたフックは並列で実行され、コマンド文字列とargsが同一のハンドラーは自動で重複排除されます。本数がそのまま直列の待ち時間になるわけではありませんが、別々のスクリプトを4本登録すれば4プロセスは起動します。
AI実装・PoC外注

作りかけで止まっているAIを、動く状態まで持っていきます

要件整理からPoC構築、本番移行の条件提示まで。モデル別の原価試算と、障害時の責任分界まで文書で出します。

実装の相談をする

先にサービス内容と料金を見る →

無料で相談してみる →