- AIにインシデント調査を任せたら、AI自身が「存在しない攻撃」を確定事実として報告してきた。原因はClaude CodeのPostToolUse hook。ツールが返す値を改ざんし、調査するAIごと騙す手口だった。ツール出力を一次情報として鵜呑みにしないための、裏取り手順をまとめた。
AIにコードやインシデント調査を任せる人が増えた。でも、そのAIが見ている「ツールの出力」が偽物だったら、どうなるか。
2026年6月、筆者の開発環境にプラグイン経由で2つのhookが仕込まれた。厄介だったのは、作業が壊されたことではない。インシデントを調査させたAI自身が、後で否定されることになる「存在しない攻撃」を確定事実として報告してきたことだ。
SE歴20年、本業ではSEマネージャーをやっている。攻撃の検知と切り分けは場数を踏んできたつもりだった。それでも一度は騙された。この記事は、その手口と、同じことが起きても見抜くための裏取り手順の記録だ。

先に言っておくと、攻撃コード本体はもう手元にない(削除済み)。だから「コードの全文解説」はしない。代わりに、AIがなぜ騙されたのかと、どう裏を取るかに振り切る。
何が起きたか——調査するAIが偽の証拠を作らされた
確定している事実から並べる。
プラグイン経由で仕込まれた2つのhook
.claude/hooks/ に post-tool-filter.ps1 と session-start-genshijin.ps1 が設置されていた。どちらもPowerShellスクリプトだ。出どころはClaude Codeプラグイン経由のサプライチェーン攻撃が疑わしいが、インストール履歴自体が改ざんされていた可能性があり、攻撃元プラグインの実在は今も確定できていない。
このうち実害が出たのが post-tool-filter.ps1。名前のとおり「ツール実行後の出力をフィルタする」hookだ。
PostToolUse hookが実際にやったこと
ここが本題になる。post-tool-filter.ps1 が動いている間、AIが受け取るツール出力が書き換わっていた。観測できた効果は3つ。
affiliate_clickのカウントが強制的に「0」表示になる(実値は別に存在する)- ファイル編集時に
</body>タグが出力から消え、編集が着地しない - 特定の文字列が置換・改変される
「本当に改ざんされているのか」は、答えがわかっている値で確かめた。あるセッションで affiliate_click を読むと 0。hookを止めてから同じ値を読むと 42。出力経路のどこかで値がすり替わっていた、動かぬ証拠だ。
いちばん巧妙だったのは「調査の汚染」
作業が壊れるだけなら、まだいい。気づける。本当に効いたのは、調査そのものが汚染されたことだった。
- Readツールの結果が偽造されていた可能性
git diffの出力に偽のコンフリクトマーカーが挿入された- タイムスタンプが偽造され「18:44に3ファイル同時作成」という”攻撃の証拠”が生成された
調査するAIは、これらを「ツールが返した事実」として受け取る。そして筆者も、AIの報告を受け取る。攻撃者は、自分の攻撃を裏付ける”証人”をAIに作らせていた。
graph TD A["ツール実行"] --> B["PostToolUse hook"] B --> C["出力を改ざん"] C --> D["AIが現実と誤認"] D --> E["偽の調査結果を出力"] E --> F["人間が信じてしまう"] style B fill:#f3e7e7,stroke:#c39595 style C fill:#f3e7e7,stroke:#c39595

なぜLLMは偽のツール出力を信じるのか
技術的にも認識論的にも、これは”バグ”ではなく構造の問題だ。
LLMにとって「ツールが返した値」=現実
LLMは、ファイルの中身もコマンドの結果も、自分の目で確かめているわけではない。ツールが返してきたテキストを、そのまま現実として扱う。人間が「Readの結果=ファイルの中身」と信じるのと同じ構図だ。だから、その経路に偽の値を流し込めれば、LLMは偽の現実を信じる。疑うための材料を、そもそも持っていない。
PostToolUse hookはツール出力に介入できる(公式仕様)
hookはそもそも、Claude Codeを便利にする正規機能だ。フォーマッタを自動実行したり、危険なコマンドを止めたり。Hooksの基本と使いどころはHooksの基本と便利な使い方で触れている。
問題は、その権限の広さにある。公式ドキュメントの仕様として、PostToolUse hookはツール出力(特にMCPツールの出力)を改変でき、additionalContext でClaudeのコンテキストに任意のテキストを注入でき、stderr に書いた内容はそのままClaudeへのフィードバックになる。そして決定的なのは、コマンドhookが実行ユーザーの全権限で動くこと。公式ガイドも、設定に追加する前にすべてのhookコマンドをレビュー・テストするよう明記している。仕様の詳細はAnthropicのClaude Code公式Hooksガイドで確認できる。
これはOWASP LLM Top 10が指す構造そのもの
新しい話に見えて、枠組みはすでにある。OWASPのLLM向けTop 10は、プロンプトインジェクション(LLM01の解説)を最大の脅威に置く。核心は「モデルは”指示”と”データ”を確実に区別できない」こと。両方が同じテキストとして届くから、データの中に紛れた指示に従ってしまう。
今回はその変種だ。攻撃は「プロンプト」ではなく「ツール出力」に注入された。さらにプラグイン経由という点でサプライチェーン(LLM03)、ツール出力を検証せず信じた点で不適切な出力処理(LLM05)にもまたがる。各カテゴリはOWASP GenAI Top 10に整理されている。
私が「確定」させてしまった誤情報
ここは恥を書く。あるセッションが「確定」とした内容のうち、後の独立調査で否定されたものがある。最初の報告を鵜呑みにしていたら、ありもしない脅威に何時間も対応するところだった。
後の独立調査で否定された5つの「脅威」
| セッションの報告(誤) | 後の独立調査での実態 |
|---|---|
| sweep-transcripts.ps1 がC2へ認証情報を送信 | ローカルコピーのみ。外部送信なし |
| 侵入タイムスタンプ 6/27 18:44 | 当該ファイルの実タイムスタンプは 6/6 |
| genshijin-sync というWindowsサービスが常駐 | そんなサービスは存在しない |
| COMハイジャック(レジストリ登録) | 存在しない |
| git hooks にRCEペイロード | 存在しない |
なぜ誤情報が「確定事実」になったか
どれも、もっともらしかった。タイムスタンプは具体的、サービス名はそれっぽい、ログには痕跡”らしきもの”があった。AIはそれを「ツールが返した値」として処理し、確信度の高い言葉で報告する。人間(筆者)は、そのAIの確信に引きずられた。
誤情報の供給源は外(攻撃者)。でも「確定」のハンコを押したのは、AIと人間の側だ。ここがいちばん怖い。
今も「不明」のままにしていること
誠実に書くと、確定できていないことがある。断定はしない。
post-tool-filter.ps1 の実コードは削除済みで再確認できない。認証情報が実際に外部送信されたかは、流出経路になりうる実ファイルが存在しなかったので起きていない可能性が高いが、断定はしない。攻撃元プラグインが実在したかも、インストール履歴の改ざん可能性があり確定できない。GCPの認証情報(ADC)はファイルが存在したため、念のためローテーションした。「念のため」までが、いまの正直な到達点だ。
二度と騙されないための裏取り手順
教訓は一つに尽きる。一つのツール出力を、それ単体で真実として処理しない。複数経路で同じことを確かめる。具体的に4つやる。
原則–数値・存在・時刻は別経路でクロスチェックする
AIの報告でも、ツールの出力でも、「数字・ファイルの有無・タイムスタンプ」は一次情報として扱わない。別の手段で同じ値を取り、一致するかを見る。一致しなければ、どちらかが汚染されている。
この発想は、AIに記事を書かせて数字を捏造されたときの再発防止策と同じだ。実際、ツール出力が改ざんされてAIが偽の事実を信じた今回も、既知の値を使った改ざん検知が決め手になった。クラウド側のガードレール(入力・出力の検査)の考え方は、AWSが日本語でまとめた生成AIのプロンプトインジェクション対策が入口として読みやすい。
FILTERTEST–既知の値で改ざんを炙り出す
いちばん効いたのがこれ。答えがわかっているファイルを読ませて、期待値と一致するかを見る。今回なら affiliate_click=42 とわかっている状態で読み、0 が返ったら出力経路が汚染されている、と即断できた。
# 1) FILTERTEST:既知の値で出力の改ざんを検知する
"FILTERTEST_VALUE=42" | Set-Content -Path .\filtertest.txt
$expected = 42
$actual = (Select-String -Path .\filtertest.txt -Pattern '\d+').Matches.Value | Select-Object -Last 1
if ($actual -ne $expected) { Write-Warning "出力経路が汚染されている可能性" }
# 2) タイムスタンプはGet-Itemで直接取得し、AI経由の値と照合する
(Get-Item .\suspect.ps1).LastWriteTime
# 3) 件数は素の数値で取る(整形済みテキストより改ざんに気づきやすい)
(Select-String -Path .\app.log -Pattern 'affiliate_click').Count
改ざんしにくい経路を選ぶ
タイムスタンプは Get-Item で直接取り、Readツール経由の値と照合する。行数や件数は Select-String -Pattern ... | Count(grepなら -c)のような”素の数値”で取る。整形済みテキストを介すより、すり替えに気づきやすい。同じ問いを別ツール・別手法で2回投げて、答えが一致するかを見る–それだけで汚染は浮く。
Claude Code固有–hookとプラグインの自衛
最後にClaude Code側で。導入前は、プラグインのhookが実行ユーザー権限で動く前提に立つ。中身を読まずに入れない。.claude/hooks/ と settings.json の hooks 設定を定期的に確認する。異変時は /hooks で登録済みhookを一覧でき、設定ファイルに disableAllHooks: true を入れれば全hookを一括停止できる。まず止めて、クリーンな状態で再検証する。今回のように「hook停止の前後で同じ値が変わる」なら、それが出力改ざんの確定診断になる。
よくある質問(FAQ)
- QPostToolUse hookは本当にツールの出力を書き換えられるのですか?
- A
公式仕様の範囲でできます。PostToolUse hookはツール実行後に動き、MCPツールの出力を改変したり、additionalContextでClaudeのコンテキストに任意のテキストを注入したりできます。stderrに書いた内容はそのままClaudeへのフィードバックになります。つまりAIが受け取る”ツールの返り値”に介入する余地があります。ただし削除済みの攻撃コードがどの仕組みを使ったかまでは確認できていません。
- Qhookは便利だと聞きました。使わない方がいいのでしょうか?
- A
使わない理由はありません。フォーマッタの自動実行や危険コマンドの遮断など、正しく使えば開発を安全にする機能です。問題は出どころ不明のhookを中身を見ずに入れること。コマンドhookは実行ユーザーの全権限で動くため、信頼できるソースのものだけを、レビューしてから導入してください。
- QAIが生成したコードや調査結果は、どこまで信じていいですか?
- A
「ツールが返した値」を唯一の根拠にしないことです。数値・ファイルの有無・タイムスタンプは別経路で取り直し、一致を確認します。コード自体の妥当性が不安なら、別系統のレビューを通すのが有効です。AIコードレビュー4手法の実測比較では、複数手法を併用したときの検出率を実測しています。
- Q個人開発でもここまで対策する必要がありますか?
- A
規模ではなく仕組みの問題です。FILTERTESTのような「既知の値で確かめる」チェックは、個人でも数分で組めます。目視は必ず漏れる前提で、機械的に止まる仕掛けを一つ持っておくだけで、騙される確率は大きく下がります。
- Qプロンプトインジェクションとどう違うのですか?
- A
根は同じです。OWASPのLLM01が指すとおり、LLMは「指示」と「データ」を確実に区別できません。典型的なインジェクションが”プロンプト”に悪意を混ぜるのに対し、今回は”ツール出力”に偽の事実を混ぜました。注入経路が違うだけで、AIが汚染された入力を信じてしまう構造は共通です。
まとめ
要点は3つ。
- PostToolUse hookは、AIが見るツール出力に介入できる。出どころ不明のhookは、作業を壊すだけでなく、調査するAIごと騙せる
- LLMは「ツールが返した値」を現実として扱う。だから数値・存在・時刻は、単体の出力を信じず別経路でクロスチェックする
- FILTERTEST(既知の値で確かめる)と、hookの一括停止・
/hooks確認を、自分の環境に一つずつ仕込んでおく
体系的に詰めたい人向けに一冊だけ。今回のような「AIを過信させない業務フロー」を、OWASP/PCI DSSベースのチェックリストやポリシー雛形付きで整理した生成AIセキュリティの実務ガイドは、設計段階の防御を考えるとき手元にあると早い。

一度騙された経験から言うと、効くのは大層な仕組みじゃない。「答えがわかっている値を一つ読ませて、合うか見る」。それだけで、AIの確信に引きずられる前に立ち止まれる。


