CASE · 中程度 · monitoring-gap
未検証の報告公開情報からの収集秘密検査のフックが「止めた」と記録した10秒後にpushが通った。exit 1では止まらなかった(未検証の報告)
筆者によれば、Claude Codeがgit pushを打つ前にPreToolUseフックで秘密の値を調べる検査を走らせていたが、7月のある朝、検査の記録に同じcommitの行が2つ並んでいた。10時04分58秒に「秘密の値の疑いで止めた」、10秒後に「pushした」である。リモートにはそのcommitが実際に届いていた。リポジトリは非公開だった。
約5分で読めます
- 発生日
- 分類
- monitoring-gap
- 影響範囲
- none
- 検知
- log-review
- タグ
- #claude-code #hooks #pretooluse #exit-code #git-push #secret-scan #silent-failure #onfailure
CH 1
原因
筆者は原因を終了コードだと考えている。検査スクリプトは止めるときにsys.exit(1)を呼んでいたが、筆者によればClaude CodeのPreToolUseフックで操作を止めるのは終了コード2だけで、2以外の0でない値は1も含めて止めないエラーとして扱われる。筆者はgit自身のpre-pushフックと同じ感覚でいた。テストも、見つけたときに1が返るかを見るだけで、操作が本当に止まるかは見ていなかったため、すべて通っていた。
CH 2
結果
筆者は11月のClaude Code 2.1.316で同じ形を作り直し、11通りを3回ずつ、計33回試した。onFailureなしでは、exit 1、exit 3、検査の例外落ち、bashに渡すファイル名の打ち間違いの4通りで、3回とも操作が走った。止まったのはexit 2と、pythonに渡すファイル名の打ち間違い(pythonが2で終わった)だけだった。操作が走った12回は、claude -pの返事がどれもフックに触れていなかった。筆者はこの形のフックが何も止めていなかったと述べている。
CH 3
対策
筆者は事故の当日に、止める所をすべて終了コード2に替えた。さらに、公式ページにある設定"onFailure": "block"(2.1.295以降)を足すと、4通りの落ち方でも操作は止まったと報告している。ただし、警告だけ出して通すつもりのexit 1のフックも止まるようになるため、足す前に探して0で終える形に替える必要がある。止める検査は、わざと引っかかる値で一度本当に止めて確かめるよう勧めている。
何が起きたか
筆者の環境では、Claude Codeがgit pushを打つ前にPreToolUseフックで検査スクリプトを走らせ、APIキーなどの秘密の値が混ざっていないかを調べていた。検査で何か見つかると、スクリプトは終了コード1で終わる作りだった。7月のある朝、筆者が検査の記録を見ると、同じcommitの行が2つあった。10時04分58秒に「秘密の値の疑いで止めた」、その10秒後に「pushした」である。リモートを調べると、そのcommitは届いていた。リポジトリは非公開だった。個人の報告で、ほかの被害には触れていない。
現場の混乱
筆者によれば、止める判定が本番で出たのはこの朝が最初で、それまでmainのリポジトリのpushは検査をすべて通っていた。つまり、止める判定が何も止めていないことは、この朝まで分からなかった。テストは、見つけたときに1が返るかを確かめる形で、操作が止まるかを見ていなかったため、すべて通っていた。
筆者は11月に原因を確かめる実験をした。Windows 11、Claude Code 2.1.316、Python 3.12.10で、フックはGit Bashから呼ばれる。終了コードの値のほかに、検査が例外で落ちる場合と設定のファイル名を打ち間違えた場合を加えて11通りを作り、claude -pで各3回、計33回試した。onFailureなしの結果は次のとおりである。
- exit 0、1、3、検査の例外落ち、bashに渡すファイル名の打ち間違い:3回とも操作が走った
- exit 2、pythonに渡すファイル名の打ち間違い:3回とも止まった
止まってほしいのに操作が走った12回では、claude -pの返事がどれもフックに触れていなかった。筆者によれば、公式ページには、対話の画面ではフックのエラーが記録に1行出るとある。筆者はclaude -pだけで試し、対話の画面では試していない。筆者は、claude -pの返事だけを読んでいると、止まらなかったことに気づけないと述べている。
原因
筆者は原因を終了コードだと考えている。検査スクリプトは止めるときにsys.exit(1)を呼んでいたが、筆者によれば、PreToolUseフックで操作を止めるのは終了コード2だけで、1を含む2以外の0でない値は止めないエラーとして扱われる。筆者は、1が失敗を表す慣習の値で、git自身のpre-pushフックなら0以外でpushが止まることから、同じだと考えていた。7月当時のClaude Codeの版は記録に残っておらず、当時の動きは確かめられないとしている。
実験では、見落としやすい形も見つかった。Pythonは捕まえていない例外で落ちると終了コード1を返すため、検査が設定ファイルを読めずに落ちると、何も検査しないまま操作が通る。また、ファイル名の打ち間違いでは、pythonは開けないファイルを渡されて2で終わり、たまたま止める扱いになった。bashは同じ打ち間違いで127を返し、操作は通った。設定の打ち間違いで止まるかどうかが、commandで呼ぶのがpythonかbashかで変わる。
対策
筆者は、事故のあった日のうちに、検査スクリプトと設定の読み込みで止める所をすべて終了コード2に替えた。2で止めると、標準エラーに書いた理由がClaudeに渡る。
もう一つは、フックの設定に"onFailure": "block"を足すことである。筆者によれば、公式のhooksのページにある項目で、2.1.295以降で使える。公式ページでは、0と2以外の終了コード、起動の失敗、時間切れを止める側に数える。このうち時間切れは筆者は試していない。onFailureを足した4通り(exit 1、例外落ち、pythonとbashのファイル名の打ち間違い)では、いずれも操作は走らなかった。ただし、警告だけ出して通すつもりでexit 1を返しているフックも止まるようになるため、足す前にそのようなフックを探し、0で終える形に替える必要があると筆者は注意している。
教訓として筆者は、止める検査を入れたら、わざと引っかかる値を置いて操作が走らないことを一度は確かめるよう勧めている。それを見るまでは、止まる作りとは呼べない。
出典
この記録をチームに回す