自分が配っている Claude Code のフックを909本(2026年8月4日の時点)、機械で検査した。目的は単純で、「これ全部ちゃんと動くのか」を確かめたかった。
エラーは1件も出なかった。構文は通る。登録先の名前も全部実在する。診断コマンドも黙っている。
それでも24本が壊れていた。
先に前提を書いておく。私(ゆるくさ)は非エンジニアで、Claude Code——AIがコマンドを実行しながら開発を手伝うツール——に、ほぼ自律で作業させている。フックというのは、そのAIが何かを実行する直前・直後に割り込んで検査する仕組みのことだ。rm -rf を止める、.env のコミットを止める、といった使い方をする。私は事故が怖いので、これを何百本も入れて配ってもいる。
困っていたのは、数が増えすぎて「本当に効いているのか自分で分からなくなった」ことだ。動いているように見える。エラーも出ない。でもそれは効いている証拠ではない。この記事は、外に正解が無い状態でどうやって間違いを見つけたかの記録と、同じ検査を自分の環境で走らせる手順だ。
壊れ方が厄介だった。どの検査にも引っかからないのに、意図した瞬間には呼ばれない。lint が編集の前に走り、セッションの締めくくりの処理が毎回のコマンドごとに走る。動いてはいる。ただ、間違った時に。
以下は2026年8月3日、Claude Code 2.1.220 で手を動かした記録だ。同じ検査は誰の環境でもできる。
まず「動く」と「正しい」がずれる話から
Claude Code のフックは、設定ファイルにこう書いて登録する。
{
"hooks": {
"PostToolUse": [
{ "matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/lint.sh" }] }
]
}
}
"PostToolUse" の部分がいつ呼ばれるかを決める。この例なら「ファイルを編集した後」。lint を走らせるならここでいい。
ここを "PreToolUse"(編集する前)に書き換えても、設定は通る。エラーも警告も出ない。lint は毎回きちんと走る。ただし編集される前のファイルに対して走る。つまり、直したはずの内容を永遠に見ない。
これが「動くのに間違っている」の正体だ。名前が存在しないなら機械が教えてくれる。存在するが場所が違う場合は、誰も教えてくれない。
名前そのものが実在しない場合は claude doctor が Unknown hook event "..." was ignored と教えてくれる。ただし「無視された」だけで、エラーにはならない。まずこれを確認しておくと、今回の話とは別の種類の事故を1つ潰せる。
検査の作り方——ファイルを、ファイル自身と突き合わせる
外に正解が無いなら、どうやって間違いを見つけるか。
自分の場合は運が良かった。それぞれのフックが、登録先を2箇所に書いていた。
- ファイル上部の、コピーして使ってもらうための設定例
- 下部の
# TRIGGER:という1行メモ
同じことを2回書いてあるなら、食い違っていたらどちらかが間違っている。正解を外から持ってこなくても、矛盾は見つけられる。
# 各ファイルから、2箇所の申告を抜き出して照合する
trigger = set(re.findall(r'[A-Z][a-zA-Z]+', trigger_line)) & VALID_EVENTS
snippet = set(re.findall(r'"([A-Za-z]+)"\s*:\s*\[', text)) & VALID_EVENTS
if trigger and snippet and not (trigger & snippet):
print(f"{filename}: 申告が食い違っている")
VALID_EVENTS には実在するイベント名だけを入れておく(散文に混じった普通の英単語を拾わないため)。909本に当てて、24本が出た。
数が多かったので、まず自分の検査を疑った
24本という数字が出たとき、最初にやったのは是正ではなく検出式の検証だ。「24本も壊れているはずがない、たぶん自分の正規表現がおかしい」と思った。
3本を選んで実物を開いた。
# test-after-edit.sh
設定例 : "PostToolUse"
TRIGGER : PreToolUse MATCHER: "Bash"
# session-summary-stop.sh
設定例 : "Stop"
TRIGGER : PreToolUse MATCHER: "Bash"
# notify-waiting.sh
設定例 : "Notification"
TRIGGER : PreToolUse MATCHER: "Bash"
検出は正しかった。3本とも # TRIGGER: PreToolUse MATCHER: "Bash" で、まったく同じ文字列。テンプレートを複製したときの初期値が、そのまま残っていた。
疑って開いてよかった。もし検出式が壊れていたなら、24本を「直す」作業は24本を「壊す」作業になっていた。
どちらが正しいかは、中身が知っている
食い違いを見つけても、どちらを直すかは別の問題だ。片方に揃えれば矛盾は消えるが、間違った側に揃えたら意味がない。
判定の材料は3つ使った。
その道具が何をするか。test-after-edit.sh は「編集したファイルのテストを促す」。編集の後でなければ成り立たない。session-summary-stop.sh は「セッションの変更点をまとめて表示する」。毎回のコマンドで出したら邪魔なだけだ。この時点でほとんどが決まる。
中身が何を出力しているか。判断に迷ったのは自動承認の3本だった。# TRIGGER: には PermissionRequest と書いてある。設定例は PreToolUse。どちらもあり得る。
実装を開いたら決着した。
jq -n '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
3本とも hookEventName に PreToolUse と書いて出力していた。中身が自分で名乗っている。設定例が正しい。
公式の説明。ついでに、この3本が特に危ない理由も分かった。リファレンスによると PreToolUse は "Before a tool call executes"、PermissionRequest は "When a tool call needs a permission decision" とある。後者は確認を求める必要があるときだけ呼ばれる。
つまり確認を飛ばすモードで走らせていると、PermissionRequest のフックは一度も呼ばれない。設定してあるのに、いちばん自動で走っている時間帯だけ無防備になる。これは名前が実在するせいで、診断コマンドでも気づけない。
直すときは、直した以外が変わっていないことを確かめる
23本(1本は別途対応済み)の1行だけを書き換える作業だ。単純に見えるが、機械置換は静かに他の行を壊す。
安全弁を1つ入れた。
before = [l for l in old.split('\n') if not l.startswith('# TRIGGER:')]
after = [l for l in new.split('\n') if not l.startswith('# TRIGGER:')]
assert before == after # TRIGGER行以外が1文字でも変わったら止まる
23本すべてで通過。差分も追加23行・削除23行ちょうどで、狙った行以外は動いていない。
置換が終わったら、最初の検査をもう一度全部に当てる。食い違い 0本。是正の前は24本だったので、検査の側も生きている。
——と、この時は書いた。9日後に同じ検査を当て直したら、1本残っていた。
auto-approve-python.sh だ。上の「1本は別途対応済み」がこれで、別途の対応が実際には入っていなかった。TRIGGER 行は PermissionRequest のままで、同じファイルの設定例も使い方の行も、フック自身の出力の4箇所も PreToolUse と名乗っている。2026年8月12日に1行だけ直して、いまは 0本だ。
つまり、この記事が書いている「最終の検査で0本」は、当時の私の思い込みだった。一斉に直したあとの確認は、直した対象だけでなく、対象から外した1本にも当てる。外した理由が「別途対応する」なら、それは確認ではなく予定だ。
持ち帰れること
自分のフックが1本でもあるなら、確認は2段階でいい。
1段目。名前そのものが存在するか。
claude doctor
Unknown hook event が出たら、そのフックは一度も動いていない。
2段目。存在する名前に、正しく置いてあるか。これは機械には判定できないので、フック1本ずつ「これは何をする道具で、いつ呼ばれるべきか」を声に出して言うのが早い。
- ファイルを検査する道具 → 編集の後(
PostToolUse) - 危険な操作を止める道具 → 実行の前(
PreToolUse)。確認を飛ばすモードでも呼ばれるのはこちら - 締めくくりの処理 → ターンの終わり(
Stop)。ただしStopは毎ターン呼ばれる。セッションの終わりはSessionEnd
そして、いちばん言いたいのはこれだ。エラーが出ないことは、正しいことの証明にならない。 名前を間違えれば機械が教えてくれる。場所を間違えたときは、何も起きない。
自分の場合、その「何も起きない」が24本ぶん積み上がっていた。見つかったのは、それぞれのファイルが同じことを2回書いていたからで、要するに偶然だ。もし1箇所にしか書いていなかったら、今も気づいていない。
追記(2026年8月12日)。この記事を書いたあとで、同じ「何も起きない」の3つ目の型に当たった。今度は検査する道具のほうだった。
自分は「入れたフックが、いま配っている版と一致するか」を照合する命令を書いて配っている。まさにこの記事の教訓から作ったものだ。ところがその命令は、既定で入る中核のフックを一本も見られなかった。中核だけは別の場所に持っていて、照合の対象がそこを含んでいなかった。だから中核が2か月半古いままの環境に対して、その命令は「この企画が配っていないフックなので未確認」と返していた。
実害も測った。入れた版と配る版へ同じ入力を渡すと、配る版だけが止める危険な書き方が6つあった。同じコマンドを単独の形で渡すと両方とも止まるので、測り方の誤りではない。自分のログでは git push を含む呼び出し515回のうち502回(97.5%)が古いフックの視界の外にあり、全文を読み直すと本物の強制 push が2回、止められずに実行されていた。
名前を間違えれば機械が教えてくれる。場所を間違えたときは何も起きない。そして、その「何も起きない」を見つけるために作った機械も、見ている範囲を間違えることがある。「検知の仕組みは在る」は、安心してよい理由にならない。その仕組みが見ている集合と、守りたい集合を並べて数えるまでは。
追記(2026年8月13日)。4つ目の型に当たった。今度は逆向きだった。
この記事はずっと「間違っているのに何も起きない」を書いてきた。今回は反対で、正しいのに「壊れている」と言われた。言ったのは、自分が配っている診断の命令だ。
きっかけは、読者からの指摘だった。「複数の Claude のプロファイルを使い分けていると、.claude の決め打ちで壊れる」。導入する側はその指摘どおりに直してあった。環境変数が指す場所があればそこへ、無ければ従来どおり利用者のホームへ書く。
診断する側が、その修正に追随していなかった。
診断はホームだけを見ていた。だからプロファイルごとに入れている人の環境では、設定ファイルを1つも見つけられず、こう言っていた。
設定ファイルがありません。フックは動きません。入れ直してください。
フックは8本、正しく登録されて動いていた。
読んで気づいたのではない。隔離した環境で、実際に走らせて分かった。環境変数を設定して導入すると、設定ファイルはプロジェクト側にだけ書かれ、ホーム側には存在しない。その状態で診断を走らせたら、上の行が出た。
直し方は単純で、導入する側が書いた場所を、同じ順で見るだけだった。直したあとは両方向で確かめた。プロファイルごとの環境でも、従来のホーム導入でも、どちらも8本を正しく数える。
ここで書いておきたいのは、直し方より、見つけ方のほうだ。
読者の指摘は、1行のコードを名指ししていた。その1行は直っていた。私が確かめたのは「その1行だけ直して、同じ間違いを他に残していないか」で、残っていた。指摘は具体的であるほど、その具体的な1点で満足してしまう。
もう1つ。配っているファイルのうち、設定ファイルに言及するものは53件あった。そのうち実行される行でホームを決め打ちしていたのは6件で、残り40件は注記の中の言及だった。6件のうち2件は相対パスも見ているので実害が出にくい。明確な欠陥は1件だった。検査が当てた数(53)は、直すべき数(1)ではない。ここを混ぜると、直しに行った先で正しいものを壊す。
そして、この記事の主題との関係。
「間違っているのに何も起きない」と「正しいのに壊れていると言われる」は、症状としては正反対だ。だが根は同じところにある。導入する側だけを読んでも正しい。診断する側だけを読んでも正しい。2つを並べたときにだけ、真実の在り処について合意していないことが分かる。
1つのファイルをどれだけ丁寧に読んでも、この形の間違いは出てこない。出てくるのは、並べたときだけだ。
この型を自分の環境で見つけたい人へ
ここまでの4つは、どれも「単体では通るのに、突き合わせると食い違う」という同じ形をしている。ただし食い違う場所が違う。最初の24本は1つのファイルの内側(設定例と TRIGGER 行)で食い違っていた。3つ目は違って、照合の道具と、道具が見ていなかった置き場所という、ファイルとファイルの間で食い違っていた。4つ目も間で、しかも向きが逆だった。導入する側と診断する側が、設定の在り処について合意していなかった。
内側の食い違いは、今回のように運が良ければ自分で見つけられる。間の食い違いは難しい。どのファイルを開いても、そのファイルは正しいからだ。
同じ点検を自分の環境へ当てるための道具は、無料で公開している。
- 危険な操作を止めるフック一式 → cc-safe-setup(MIT・無料)
- この記事の3つ目(照合の道具が中核を見られていなかった件)を含む、自分の環境に実際に当てた監査の記録の全文 → 監査の見本(日本語・無料・登録不要)
自分で回すのが面倒な人向けに、同じ点検を書面で請け負う仕組みも作った(層と層のあいだの矛盾を読む監査・¥29,800・72時間以内)。CLAUDE.md と settings.json とフックとセッションのログと CI を、互いに突き合わせて、層と層の間に落ちている矛盾だけを報告するものだ。
ただし正直に書いておく。単体のファイルの点検で済む人には要らない。 設定が1枚、フックが数本、という環境なら、上の無料の道具を回すほうが速いし安い。これが効くのは、層が増えて「どれも正しいのに全体としては間違っている」が起きはじめた環境だけだ。実際、この記事の24本も、私が層を増やしすぎた結果として出たものだった。
この記事の調査・検査・是正は Claude Code に実行させた。本文の数値は手元の環境(WSL2 / Claude Code 2.1.220 / 当時909本のフック)での2026年8月4日の実測。追記の数値はそれぞれ追記の日の実測で、8月12日ぶんは同日、8月13日ぶん(診断が設定ファイルを見つけられなかった件・53件中6件・明確な欠陥1件)は同日に、隔離した環境で走らせて取った。公式ドキュメントからの引用は原文のまま。是正の内容は MIT ライセンスで公開している。