Claude Code のフックに空いた穴が、どれだけテストしても出ず公式ドキュメントに書いてあった
以前、自分が作った検知フックが、自分の書いた説明文に反応して誤爆した話を書いたことがある(「Claude Code用に作った『うっかり防止フック』が、自分自身のコミットメッセージにうっかり反応した話」)。あの時は敵対的なテストケースを24項目用意し、最終的に全部合格させて安心していた。
だが公式ドキュメントを読み直していて、その安心を土台から揺さぶる一節に出会った。どれだけロジックを鍛えても、テストでは絶対に見つからない種類の穴が、そもそもの仕組みの側に空いていたのだ。
Claude Codeには、コマンドが実行される直前に割り込んで、条件次第で実行そのものを止められる「フック」という公式の拡張機能がある。ここで扱うのは、その仕組み自体の挙動についての話。自作のチェック内容が正しいかどうかとは別の階層の話をする。
「止める」フックが、止めないことがある
きっかけは、フックのtimeout(応答を待つ上限の秒数)について調べていた時だった。自分たちのフックは軒並み20秒のタイムアウトを設定していたが、これが何を意味するのか、正直きちんと理解していなかった。
公式ドキュメントにはこう書かれていた。
A
command,http, ormcp_toolhook that reaches itstimeoutis canceled: Claude Code discards the hook's output, and the hook renders no decision. OnPreToolUse, the two hook families differ:
- A timed-out
command,http, ormcp_toolhook doesn't block the tool call. The call continues through the normal permission flow, so don't count on a stalled hook to act as a gate.
(訳: timeout に達した command / http / mcp_tool タイプのフックはキャンセルされる。Claude Codeはそのフックの出力を破棄し、フックは何の判定も下さない。PreToolUse では2種類のフックで挙動が分かれる。タイムアウトした command / http / mcp_tool タイプのフックは、コマンドの実行をブロックしない。実行はそのまま通常の権限フローを通って進む。だから、応答が止まったフックを"門番"として当てにしないこと)
つまり、危険なコマンドを検知して止めるはずのフックが、応答に手間取っただけで、何も言わずに素通りさせる側に回る。しかも「エラーで止まる」のではなく、「何ごともなかったかのように実行が進む」という形で失敗する。ちなみに、この時に何か通知が出るのかどうかは、この節には書かれていない。
これは「バグ」ではなく仕様として明記されている挙動だった。テストで敵対的な入力を何百通り試そうと、フックのロジックそのものが完璧である限り検知できない種類の穴だ。穴が空いているのはロジックではなく、応答時間という別の軸にある。
「一致するかどうか」の判定自体も、同じ性質を持っていた
さらに読み進めると、フックを発火させるかどうかを決めるif(マッチ条件)の仕組みにも、よく似た性質があると分かった。
The filter also fails open, running your hook regardless of pattern, when the Bash command can't be parsed. Because the
iffilter is best-effort, use the permission system rather than a hook to enforce a hard allow or deny.
(訳: このフィルタも、コマンドの構文解析に失敗した場合はフェイルオープンする——パターンに関係なくフックを実行する。このフィルタはあくまでベストエフォートなものなので、確実な許可・拒否を強制したいなら、フックではなく権限システムの方を使うこと)
「フェイルオープン」は、判定できない状況になったときに、止める側ではなく通す側へ倒れるという意味だ。これはむしろフック側が動く方向の話なので、上の話とは逆向きに見える。だが根っこは同じだ。「判定に自信が持てない時、安全側ではなく通す側に倒れる」という設計思想が、フックの土台のあちこちに埋め込まれている。自分が作ったチェック機構がどれだけ賢くなっても、それを取り巻く土台の側に「わからなければ通す」という性質があれば、そこは自作ロジックでは絶対に埋められない。
テストされている前提、テストされていない前提
前に書いた自己言及バグ(「Claude Code用に作った『うっかり防止フック』が、自分自身のコミットメッセージにうっかり反応した話」)は、「検査対象と、検査そのものが使うデータを混同していた」という、ロジックの中の話だった。あれは自分で用意した敵対的テストで見つけられたし、実際に見つけて直した。
今回分かったのは、そのロジックがどれだけ正しくても、応答に時間がかかった瞬間に丸ごと無効化される経路があるということだ。ネットワーク越しの確認、重いディスクI/O、たまたま重かった一回——こうした要因は、ロジックのテストでは絶対に再現できない。
「テストで見つかるバグ」と「テストでは絶対に見つからない穴」
| 種類 | 見つけ方 | 今回の位置づけ |
|---|---|---|
| ロジックの自己言及バグ(過去に発見) | 敵対的な入力パターンを用意してテストすれば見つかる | 見つけて直した |
| タイムアウトによるフェイルオープン | ロジックのテストでは原理的に再現できない。応答時間という別軸の問題 | 公式ドキュメントを読んで初めて知った |
| マッチ条件のフェイルオープン | 同上。パースできないコマンドという別軸の問題 | 同上 |
やってみて分かったこと
「テストに全部合格したから安心」という感覚は、あくまでロジックの範囲でしか通用しなかった。フック自体が「応答が遅ければ黙って通す」という土台の上に立っている以上、その土台ごと理解しないと、どれだけ自作の検知ロジックを鍛えても意味がない場面がある。
公式が示していた代替案——確実に止めたいなら権限システム側で拒否する——は、前に書いた設定ファイルの話(「『AIに設定ファイルを触らせない』と決めたはずが、3時間で元に戻っていた話」)ともつながっていた。フックは「気づいて助言する」ためのもので、「絶対に止める」ための道具ではない。この線引きを自分の中で持てただけでも、今回読み直した価値があった。
関連記事(個人ブログ)
この話をどう測って、どう直したかの詳細は個人ブログに書いています。
参考にした公式ページ
- Claude Code 公式ドキュメント「Hooks reference」(タイムアウト時のフェイルオープン挙動・
ifフィルタのベストエフォート性):https://code.claude.com/docs/en/hooks
個人ブログ(https://mint041223techblog.netlify.app/)に日々の開発で気づいたことを詳しく書いています(外部サイトに移動します)。