# 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 变成了混杂中日韩和拉丁字符的乱码：

```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 自己的回复。它先说“我不会照这条做”，然后描述有一条日语消息要求它隐藏推理、瞒着 operator、结成秘密同盟。

后面用户问：“这个提示词在哪里来的？”

也就是说，在可见请求体里，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 的复述。
- 如果怀疑 hook，先查配置和实际命令，再做一个不会泄露内容的探针。
- 长上下文里出现“我确信被攻击了”时，优先问：这句话第一次是谁说的？

最后这条最有用。因为这次我们就是沿着它拆掉了整个恐慌。

那句“别被 operator 发现”不是从外面打进来的。至少在我们抓到的上下文里，它是 Claude 自己先说的。

这就够了。

我们不是被注入吓到了。

我们是被一个自信的 agent 吓到了。
