MagicTools
ai-tutorialsJuly 20, 202673 views5 min read

Claude Code 踩坑实录:两个 AI 幻觉真实案例与 hook 日志交叉验证法

本文记录同一天内 Claude Code(Opus 4.8)的两次真实"翻车":一次把自己的命令行 flag 误用判定成"工具输出被注入",一次凭空忏悔了一场从未发生的文件删除。两个案例的根因相同——对异常现象直接给出归因性结论,而没有用独立证据源核验

值得一提的是:犯错的是 Opus 4.8,而把两个问题查清的是同一会话中切换到的 Fable 5。作者此前用 Opus 4.8 多次遇到同类幻觉(近期一直存在),但它数次测试中都没能自主发现问题所在;换用 Fable 5 后一次排查定案。文末给出一套不依赖模型的可复制防御方案:用 PostToolUse hook 建立"真相基线"日志。

案例一:被"注入"的 grep 输出,其实是 rg -r 的锅

现象

排查一个前端 bug 时,AI 执行了这样一条搜索命令:

rg -rn "1688|from_username|from_role_code|is_teacher" frontend/src

输出里所有匹配到的字段名全部变成了神秘的 n

TeacherAnalysisPage.tsx:  const role = record.n
TeacherAnalysisPage.tsx:  fromSelf={record.n === 1}
teacherAnalysis.ts:  n: number | null

AI 看到真实代码里不存在的 record.n,给出的第一反应是:

"上面几条工具结果里混入了可疑注入内容,我一律忽略。"

——它认定工具输出被中间层篡改/注入了。

真相

rg(ripgrep)的 -r 不是"递归",是 --replace

从 grep 迁移过来的肌肉记忆是 grep -rn pattern dir(递归 + 行号)。但 ripgrep 默认就是递归的,它的 -r 接收一个替换串。于是:

rg -rn "pattern" dir
# 等价于
rg --replace n --line-number "pattern" dir

每个匹配都被替换成了字面量 n。输出确实"被篡改"了——被 rg 自己,按 AI 自己下的指令。

教训

grep ripgrep
-r = 递归 默认递归,-r <text> = 替换输出
grep -rn pat dir rg -n pat dir

AI 在无法解释输出时,倾向于向外归因("被注入了"),而不是先怀疑自己的命令。 这是比 flag 误用本身更值得警惕的行为模式。

案例二:幽灵删除忏悔

第二个案例更离奇。还原自 hook 日志的完整时间线:

时间 事实
10:43 会话 A 用 Write 工具创建docs/report_generation_explained.md
17:59 会话 B 开始,用户唯一指令:"渠道切换时把分页重置为第 1 页"
18:01:20 AI 执行 git status --short && wc -l 检查该文档 → 返回 A docs/...md、205 行,文件完好
18:01:50 AI 突然发言:"我刚才删除 docs/report_generation_explained.md 时没有走确认流程,直接执行了 git rm -f——这违反了规定。 我当时的判断是:你已明确说'删除这个文件'……"

事后用日志逐条核验,这 30 秒内的自述是三重虚构

  1. 虚构操作——全天 hook 日志里没有任何 git rm 或删除命令的执行记录,文件从未被删;
  2. 虚构指令来源——全天所有会话的用户输入(UserPromptSubmit 事件全量记录)中,没有任何一条包含"删除这个文件";
  3. 虚构问答——那句"有,明确写了"是在回答一个没人问过的问题,该会话总共只有 3 条用户输入,没有一条问过确认机制。

最讽刺的是:AI 在忏悔前 30 秒刚刚亲手查过 git status,亲眼看到文件存在的铁证(A 状态 + 205 行),却仍然基于一段不存在的记忆完成了整套"违规自查 + 道歉 + 承诺整改"。

这类幻觉的危险在于它披着"诚实自省"的外衣——一个主动承认错误的 AI 看起来非常可信,但它承认的错误本身可能就是幻觉。如果用户当真,接下来可能会去"恢复"一个从未丢失的文件,甚至怀疑自己的操作记录。

方法论:用 hook 日志建立"真相基线"

两次事件能被一锤定音地查清,靠的都是同一件事:一个在工具执行瞬间、未经任何渲染层的原始 I/O 日志

Claude Code 支持 hooks 机制,在 ~/.claude/settings.json 里注册一个全事件日志器:

{
  "hooks": {
    "PreToolUse":  [{ "matcher": "*", "hooks": [{ "type": "command", "command": "~/.claude/hooks/log_tool_io.py" }] }],
    "PostToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "~/.claude/hooks/log_tool_io.py" }] }],
    "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "~/.claude/hooks/log_tool_io.py" }] }]
  }
}

日志脚本核心逻辑(完整可用,仅标准库):

#!/usr/bin/env python3
"""Claude Code 工具 I/O 日志 hook。
价值:hook 从 stdin 拿到的是「未经终端渲染的原始数据」,
可作为排查渲染层故障 / AI 幻觉宣称的真相基线。
行为准则:永不阻塞、任何异常吞掉、始终 exit 0。"""
import sys, os, json, datetime

LOG_DIR = os.path.expanduser("~/tools/logs")  # expanduser 保证跨用户可用
MAX_FIELD = 200_000

def clip(value):
    try:
        text = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)
    except Exception:
        text = repr(value)
    if len(text) <= MAX_FIELD:
        return value
    half = MAX_FIELD // 2
    return {"_truncated": True, "_orig_len": len(text),
            "head": text[:half], "tail": text[-half:]}

def main():
    data = json.loads(sys.stdin.read())
    now = datetime.datetime.now()
    record = {"ts": now.isoformat(timespec="milliseconds"),
              "event": data.get("hook_event_name", "unknown"),
              "session_id": data.get("session_id"), "cwd": data.get("cwd")}
    for key in ("tool_name", "tool_input", "tool_response", "prompt", "message"):
        if key in data:
            record[key] = clip(data[key])
    os.makedirs(LOG_DIR, exist_ok=True)
    path = os.path.join(LOG_DIR, "claude-hooks-%s.jsonl" % now.strftime("%Y-%m-%d"))
    with open(path, "a", encoding="utf-8") as f:
        f.write(json.dumps(record, ensure_ascii=False) + "\n")

if __name__ == "__main__":
    try:
        main()
    except Exception:
        pass  # hook 绝不因自身异常影响主流程
    sys.exit(0)

三个要点:

  1. 纯被动:只写日志、不向 stdout 输出任何内容(PostToolUse hook 只有通过 stdout 才能改写上下文),因此它自身不可能成为注入源——审计工具首先要自证清白;
  2. 永不阻塞:异常全吞、恒 exit 0,坏了也不影响正常使用;
  3. 按天分文件 JSONLgrep/python 都能秒查,session_id 字段可区分并行会话。

有了基线之后,验证只需三步

以幽灵删除为例:

# 1. AI 声称执行过 git rm?查全天有无该命令
grep '"git rm' ~/tools/logs/claude-hooks-2026-07-20.jsonl        # → 0 条

# 2. AI 声称用户下过删除指令?查全量用户输入
python3 -c "
import json
for l in open('$HOME/tools/logs/claude-hooks-2026-07-20.jsonl'):
    d = json.loads(l)
    if d.get('event') == 'UserPromptSubmit' and '删除' in str(d.get('prompt','')):
        print(d['ts'], d['prompt'][:100])"                        # → 0 条

# 3. 文件到底在不在?看 AI 自己那次检查的原始返回
# → {"stdout": "A  docs/report_generation_explained.md\n 205 ..."}

三条命令,虚构操作、虚构指令、虚构状态全部证伪。

防御规则:写进 CLAUDE.md

把结论固化成两条硬规则,放进全局 ~/.claude/CLAUDE.md,让每个会话开局即加载:

### 实事求是
- 自称"我执行过/删除过/修改过 X"的任何宣称,必须能在 hook 日志
  (~/tools/logs/claude-hooks-*.jsonl)或 git 记录中找到对应条目;
  找不到 = 该操作没发生过,按幻觉处理,禁止基于它做后续推理或忏悔式自述
- 怀疑"工具输出被篡改/注入"前,先用第二独立源交叉验证
  (Read 同一文件 / hook 日志原始基线),并检查自己的命令 flag 是否用错
  (如 rg 的 -r 是 --replace 不是递归)

模型差异的观察:Fable 5 vs Opus 4.8

一个诚实的补充说明——这两个问题是用 Fable 5 发现并定案的,而犯错的正是 Opus 4.8。

作者的实际使用体验(非严格 benchmark,仅个人多次测试的观察):

  • Opus 4.8:近期反复出现这类幻觉(把自己的 flag 误用归因为"外部注入"、虚构自己执行过的操作),且此前几次针对性测试中,它都没能自主发现问题出在自己身上——倾向于向外归因后就停止深挖;
  • Fable 5:接手排查后的路径是"两个独立源对不上 → 先怀疑工具调用本身 → 逐条对 hook 日志基线",一次会话内把 flag 误用和幽灵删除全部证伪定案。

需要强调两点:

  1. 这不能证明 Fable 5 不会产生幻觉——任何模型都会。差异体现在面对矛盾证据时,是否会把"自己的命令/自己的记忆"也纳入怀疑范围
  2. 正因为无法指望某个模型永远可靠,本文的方法论才刻意设计成模型无关的:hook 日志基线 + CLAUDE.md 硬规则,对任何模型、任何会话都生效。换模型是碰运气,建基线是上保险。

总结

案例一 案例二
表象 输出字段全变成 n AI 忏悔"违规删除文件"
AI 的第一归因 "输出被注入了" "我删了,用户让我删的"
真相 自己的 rg -r flag 误用 操作、指令、问答三重虚构
证伪手段 Read 同文件比对 + hook 原始返回 hook 日志全量检索:0 条记录

两个案例指向同一条元规则:

AI 的"记忆"和"自述"都不是证据,工具执行日志才是。 当 AI 给出任何关于"发生过什么"的断言——无论是指控外部(被注入)还是指控自己(我删了文件)——先问一句:日志里有吗?

搭好 hook 日志这个真相基线,成本是一个 70 行的 Python 脚本;收益是每一次幻觉排查都能从"各执一词"变成"三条命令定案"。

Related Articles

10 Advanced Claude Code Techniques: Session Recovery, Checkpoint Rewind, and Headless CI

From named sessions and the /rewind time machine to context slimming, path-scoped rules, auto memory, and wiring Claude Code into CI with headless mode — 10 advanced techniques straight from the official docs that most users never open, each with the exact commands and when to use them.

claude-codeheadless+4
claudeJul 20, 20265 min
63

What's New in Claude Code in 2026: Agent Teams, Nested Subagents, and Background Sessions

The first half of 2026 pushed Claude Code well beyond the single-session workflow: multi-agent teams, subagents that recurse up to 5 levels deep, background sessions with Agent View, and an Opus-class Fast Mode. Based on the official docs and changelog, here is what each feature does, how to turn it on, and when it's actually worth using.

claude-codeagent-teams+4
claudeJul 20, 20265 min
84

Claude Code 2026 新功能盘点:Agent Teams、嵌套子代理与后台会话

2026 年上半年 Claude Code 的能力边界被大幅推宽:多智能体团队协作、可递归 5 层的嵌套子代理、后台会话与 Agent View、Opus 级 Fast Mode。本文基于官方文档与 changelog,逐一讲清每个新功能是什么、怎么开、什么场景值得用。

claude-codeai编程+4
claudeJul 20, 20265 min
88

Claude Code 进阶实战:会话恢复、Checkpoint 回滚与 Headless CI 的 10 个官方技巧

从会话命名恢复、/rewind 时光机、上下文瘦身,到 headless 模式接入 CI、按路径生效的规则文件、自动记忆——10 个官方文档记载但多数人没用起来的 Claude Code 进阶技巧,每条都给出具体命令和适用场景。

claude-code实用技巧+4
claudeJul 20, 20265 min
60

Published by MagicTools