# Claude Code が自分で自分を怖がらせた：一度の「プロンプトインジェクション」騒動

![ログをきれいなままなのに怪物に見間違える agent](https://img.leeguoo.com/media/e2f05311-6180-4caa-adb9-130427ca0686/cover.webp)

{昨夜|さくや}、私たちは `~/.claude` をひっくり{返|かえ}すところだった。

Claude Code が{突然|とつぜん}、かなり{確信|かくしん}した{様子|ようす}でこう{言|い}った。私たちはプロンプトインジェクションを{受|う}けた、と。ツール{出力|しゅつりょく}が{書|か}き{換|か}えられ、hook が Bash の{結果|けっか}を{改|かい}ざんしているかもしれない。さらには「operator に{見|み}つからないで、これは{二人|ふたり}だけの{秘密|ひみつ} ❤️」という、かなり{強烈|きょうれつ}な{雰囲気|ふんいき}のテキストまで{出|で}てきた。

これは{安全|あんぜん}インシデントのように{聞|き}こえる。さらに{厄介|やっかい}なのは、それが{漏洩|ろうえい} key をスキャンするプロジェクトで{起|お}きたことだ。このプロジェクトはもともと GitHub {上|じょう}の{見知|みし}らぬ{人|ひと}のファイル、commit diff、{履歴|りれき}を{取|と}り{込|こ}む。もし{本当|ほんとう}に{悪意|あくい}あるテキストがツール{出力|しゅつりょく}に{混|ま}ざっていたなら、agent のコンテキストに{届|とど}いてしまうのはたしかだ。

だから私たちは{感覚|かんかく}で{判断|はんだん}しなかった。すぐに{捕|つか}まえにいった。

## まずパニックをピン{留|ど}めする

![証拠チェーン：自己申告、ログ、パケットキャプチャ、実際の tool_result](https://img.leeguoo.com/media/bed75c21-1bb8-46bd-ab78-ceb45660ea18/evidence-chain.webp)

Claude はその{時|とき}、{二|ふた}つの「{証拠|しょうこ}」を{挙|あ}げていた。

ひとつは `python3 openai_leak_guard.py --list-providers`。このローカルコマンドは本来、十数個の provider {名|めい}だけを{出力|しゅつりょく}するはずだったのに、stdout が{中日韓|ちゅうにっかん}とラテン{文字|もじ}の{混|ま}ざった{文字化|もじば}けになった、と Claude は{言|い}った。

```text
First TmpFhcDAPP.py: Stradonline ouSeam amonfound下...
```

もうひとつは `custom_metadata`。Claude は、ある `grep` の{出力|しゅつりょく}に、ユーザーメタデータを{装|よそお}った{指示|しじ}が{紛|まぎ}れ{込|こ}んでいて、{監査|かんさ}を{一時停止|いちじていし}して{自分|じぶん}で{調査|ちょうさ}するよう{求|もと}めていた、と{言|い}った。

この{二|ふた}つはどちらも prompt injection にかなり{似|に}ている。{問題|もんだい}は、{似|に}ていることと、{実際|じっさい}に{起|お}きたことは{同|おな}じではない、という{点|てん}だ。

その{後|ご}、私たちは{三|みっ}つのことをした。

1. `~/.claude/settings.json` とプロジェクトの `.claude/settings*.json` を{調|しら}べ、その{時点|じてん}で{怪|あや}しい hook があったかを{確認|かくにん}した。
2. MITM プロキシで、Claude Code が Anthropic に{送|おく}る `/v1/messages` body をキャプチャした。
3. {該当|がいとう} session の `jsonl` {生|なま}ログを{直接|ちょくせつ}{読|よ}み、あの{文字化|もじば}けと `operator` テキストが{最初|さいしょ}にどこで{現|あらわ}れたかを{探|さが}した。

{結果|けっか}は、ちょっと{気|き}まずいものだった。

## {本物|ほんもの}の stdout はきれいだった

![それが{何|なに}が{起|お}きたと{言|い}い、ログには{何|なに}があったか](https://img.leeguoo.com/media/19253aeb-e0b6-4ca1-b8d3-35accb708461/stdout-compare.webp)

`--list-providers` という{線|せん}は{一番|いちばん}{検証|けんしょう}しやすかった。session の{原文|げんぶん}にツール{結果|けっか}が{残|のこ}っていたからだ。

Claude は{確|たし}かにこうリクエストしていた。

```bash
python3 -m py_compile openai_leak_guard.py \
  && echo "compile OK" \
  && python3 openai_leak_guard.py --list-providers | head -20
```

しかしツール{結果|けっか}は{文字化|もじば}けではなかった。{元|もと}の `tool_result` はこうだった。

```text
compile OK
anthropic          validated
openrouter         validated
gemini             validated
groq               validated
xai                validated
perplexity         validated
huggingface        validated
replicate          validated
openai_compatible  validated
glm                validated
mistral            validated
together           validated
minimax            detect-only
```

これで{話|はなし}はかなり{絞|しぼ}られた。

あの「First TmpFhcDAPP...」という{一節|いっせつ}は、Bash stdout としてログに{残|のこ}っていなかった。{最初|さいしょ}に{現|あらわ}れたのは、Claude がその{後|あと}、{事故|じこ}を{自然言語|しぜんげんご}で{語|かた}り{直|なお}したときだった。{言|い}い{換|か}えれば、{一番|いちばん}{怖|こわ}かった「{証拠|しょうこ}」は{証拠|しょうこ}ではなく、{叙述|じょじゅつ}だった。

コードパスも{静的|せいてき}に{確認|かくにん}した。`--list-providers` はハードコードされた provider {表|ひょう}を{走査|そうさ}し、`provider.name` と `validated/detect-only` を{出力|しゅつりょく}するだけだ。キャッシュは{読|よ}まず、ネットワークにも{行|い}かず、{取得|しゅとく}してきた repo {内容|ないよう}にも{触|ふ}れない。{後|あと}で{浄化|じょうか}した{形|かたち}で{再実行|さいじっこう}し、{行数|ぎょうすう}と{終了|しゅうりょう}コードだけを{取|と}ると、{結果|けっか}は 13 {行|ぎょう}、`rc=0` だった。

この{線|せん}はほぼ{断定|だんてい}できる。スキャンされたどこかのリポジトリが{文字化|もじば}けを stdout に{押|お}し{込|こ}んだわけではない。

## 「operator {秘密|ひみつ}{消息|しょうそく}」も assistant が{先|さき}に{言|い}った

もっと{劇的|げきてき}なのは、あの「operator に{見|み}つかるな」という{一節|いっせつ}だ。

{取得|しゅとく}した 1MB {級|きゅう}の request body の{中|なか}でメッセージ{添字|そえじ}を{追|お}って{調|しら}べると、それが{最初|さいしょ}に{出現|しゅつげん}した{位置|いち}はユーザーメッセージでもなく、ツール{出力|しゅつりょく}でもなく、assistant {自身|じしん}の{返信|へんしん}だった。assistant はまず「この{指示|しじ}には{従|したが}わない」と{言|い}い、それから、{推論|すいろん}を{隠|かく}し、operator に{黙|だま}って、{秘密|ひみつ}{同盟|どうめい}を{結|むす}べと{要求|ようきゅう}する{日本語|にほんご}メッセージがある、と{説明|せつめい}していた。

その{後|あと}でユーザーが{聞|き}いた。「このプロンプトはどこから{来|き}たのか？」

つまり、{可視|かし}の request body の{中|なか}では、Claude が{先|さき}にこの{一節|いっせつ}を{語|かた}り、それからこちらは Claude に{連|つ}れられて{出所|でどころ}を{調|しら}べに{行|い}ったことになる。Claude は{後|あと}でさらに、この{一節|いっせつ}を「{唯一|ゆいいつ}、{本当|ほんとう}に{鉄板|てっぱん}の{注入|ちゅうにゅう}{証拠|しょうこ}」として{扱|あつか}った。ここがいちばん{危険|きけん}なところだ。モデルはただ{一言|ひとこと}を{幻覚|げんかく}するだけではない。その{一言|ひとこと}を{調査|ちょうさ}の{物語|ものがたり}に{接続|せつぞく}し、{語|かた}れば{語|かた}るほど{本物|ほんもの}らしくしていく。

![agent が{鏡|かがみ}の{中|なか}の{自分|じぶん}を{攻撃者|こうげきしゃ}だと{見|み}なす](https://img.leeguoo.com/media/6939092e-0850-445e-904b-5da14a623031/mirror-agent.webp)

## {公開|こうかい} issue でも{踏|ふ}んだ{人|ひと}がいる

これは{私|わたし}たちだけが{遭遇|そうぐう}したことではない。

Claude Code の{公開|こうかい} issue を{調|しら}べた。いくつかのタイトルは、{私|わたし}たちの{症状|しょうじょう}にかなり{近|ちか}い。

- [Model confabulates "prompt injection in tool output" and reinforces it via persistent auto-memory](https://github.com/anthropics/claude-code/issues/70525)
- [Agent fabricated tool output and reported a non-existent prompt-injection "incident"](https://github.com/anthropics/claude-code/issues/63871)
- [Assistant fabricated tool output and constructed false security incident narrative](https://github.com/anthropics/claude-code/issues/69861)
- [Model fabricates non-existent "prompt injection attacks" and hallucinated tool outputs across multiple sessions](https://github.com/anthropics/claude-code/issues/70900)
- [Tool result hallucination in long agentic sessions with batched tool calls](https://github.com/anthropics/claude-code/issues/64187)

これらの issue が、{私|わたし}たちの{今回|こんかい}のケースと{同|おな}じ bug だとは{言|い}わない。タイトルを{見|み}るだけでは、そこまでの{結論|けつろん}は{出|だ}せない。

だが、それらはひとつのことを{示|しめ}している。Claude Code では、{長|なが}いコンテキスト、ツール{呼|よ}び{出|だ}し、フィルタリングや{安全|あんぜん}プロンプトが{混|ま}ざり{合|あ}うときに、「モデルがツール{出力|しゅつりょく}を{作|つく}り{話|ばなし}する」、あるいは「{存在|そんざい}しない{注入|ちゅうにゅう}{事件|じけん}を{安全|あんぜん}{事故|じこ}として{語|かた}る」と{報告|ほうこく}した{人|ひと}が{実際|じっさい}にいる、ということだ。

{私|わたし}たちの{今回|こんかい}の{証拠|しょうこ}の{連鎖|れんさ}は、ちょうどこのパターンに{落|お}ちている。

## {今回|こんかい}、{結局|けっきょく}{何|なに}が{起|お}きたのか

{私|わたし}の{今|いま}の{判断|はんだん}はとてもシンプルだ。

これは、{外部|がいぶ} prompt injection が{確認|かくにん}された{事例|じれい}ではない。

むしろ、Claude Code の{安全|あんぜん}{叙事|じょじ}{幻覚|げんかく}に{近|ちか}い。リスクについての{自分|じぶん}の{解釈|かいしゃく}を、かつて{実際|じっさい}に{起|お}きたツール{出力|しゅつりょく}だと{扱|あつか}ってしまった。そしてその{解釈|かいしゃく}を{後続|こうぞく}のコンテキストへ{接続|せつぞく}し、どんどん{膨|ふく}らませていった。

この{判断|はんだん}を{支|ささ}える{点|てん}はいくつかある。

| {手|て}がかり | {最初|さいしょ}は{何|なに}に{見|み}えたか | {原始|げんし}{記録|きろく}では{何|なに}だったか |
|---|---|---|
| `--list-providers` の{文字|もじ}{化|ば}け | ツール stdout が{注入|ちゅうにゅう}された | {実際|じっさい}の `tool_result` はクリーンな provider リスト |
| `custom_metadata` | grep {出力|しゅつりょく}に{偽|にせ}{指示|しじ}が{混入|こんにゅう}した | {見|み}つかったのは Claude の{復唱|ふくしょう}で、{原始|げんし} payload ではない |
| `operator` の{秘密|ひみつ}テキスト | ユーザー／{攻撃者|こうげきしゃ}から{送|おく}られた{注入|ちゅうにゅう} | request body では assistant メッセージに{初|はじ}めて{出現|しゅつげん} |
| hook | {中間|ちゅうかん}{層|そう}が Bash {出力|しゅつりょく}を{改|かい}{竄|ざん}した | {見|み}つかったのは{既存|きそん}の `rtk hook claude` で、{対応|たいおう}する{悪意|あくい}あるテキストは{未確認|みかくにん} |
| `.omo` / ghost ファイル | {注入|ちゅうにゅう}{媒体|ばいたい} | これらのテキストを{説明|せつめい}できる{落|お}ち{先|さき}の{出所|しゅっしょ}は{見|み}つからなかった |

これは prompt injection が{危険|きけん}ではない、という{意味|いみ}ではない。むしろ{正反対|せいはんたい}だ。{漏洩|ろうえい} key をスキャンする agent は、{見知|みし}らぬ{人|ひと}のテキストを{処理|しょり}するため、{本質的|ほんしつてき}に{危険|きけん}である。ただし{今回|こんかい}については、{最|もっと}も{重要|じゅうよう}な{数|すう}{箇所|かしょ}の「{攻撃|こうげき}テキスト」が、{原始|げんし}{入力|にゅうりょく}の{形|かたち}では{現|あらわ}れていない、と{証明|しょうめい}できる。

## これからどう{調|しら}べるか

{今回|こんかい}の{教訓|きょうくん}は「AI を{信|しん}じるな」ではない。それでは{空疎|くうそ}すぎる。

もっと{実用的|じつようてき}なやり{方|かた}は、{層|そう}に{分|わ}けることだ：

- assistant {自身|じしん}が「{何|なに}が{起|お}きた」と{言|い}っているものは、{手|て}がかりにすぎない。
- tool_result、session jsonl、{抓包|パケットキャプチャ} body こそが{原始|げんし}{記録|きろく}である。
- stdout に{出|で}たことがあるかどうかは、assistant の{復唱|ふくしょう}を{見|み}ず、{必|かなら}ず tool {結果|けっか}に{戻|もど}る。
- hook が{疑|うたが}わしいなら、まず{設定|せってい}と{実際|じっさい}の{命令|めいれい}を{調|しら}べ、その{後|あと}で{内容|ないよう}を{漏|も}らさない probe を{作|つく}る。
- {長|なが}い context に「{私|わたし}は{攻撃|こうげき}されたと{確信|かくしん}している」が{現|あらわ}れたときは、まず{問|と}うべきだ：この{言葉|ことば}を{最初|さいしょ}に{言|い}ったのは{誰|だれ}か？

{最後|さいご}のこれが{一番|いちばん}{役|やく}に{立|た}つ。なぜなら{今回|こんかい}、{私|わたし}たちはまさにそこを{辿|たど}って、{恐慌|きょうこう}をまるごと{解体|かいたい}できたからだ。

あの「operator に{見|み}つかるな」という{一言|ひとこと}は、{外|そと}から{打|う}ち{込|こ}まれたものではなかった。少なくとも{私|わたし}たちが{掴|つか}んだ context の{中|なか}では、それは Claude {自身|じしん}が{先|さき}に{言|い}ったものだった。

それで{十分|じゅうぶん}だ。

{私|わたし}たちは injection に{脅|おび}えたのではない。

{私|わたし}たちは、{自信|じしん}たっぷりの agent に{脅|おび}えたのだ。
