跳到主要内容
它引用了 AGENTS.md 第 7-10 行,然后你把这句话抄进了审计记录

它引用了 AGENTS.md 第 7-10 行,然后你把这句话抄进了审计记录

0x7F
0x7F

· 阅读约 7 分钟

一条 AI 代码审查的评论,长这样:

High severity — rule violation
AGENTS.md L7-10 / CLAUDE.md L7-10
新增路由 /feedbackSummary 违反仓库命名约定(要求 kebab-case)。
客户端用正确的 kebab-case URL 会拿到 404,用错误的 camelCase 反而可用。

这里面最像证据的不是严重性标签——那个谁都能贴;也不是那段推理——推理可以是猜的。是行号。AGENTS.md 第 7 到第 10 行,指名道姓。它暗示了一件事:这个工具打开过你仓库里的规则文件,读到了第 7 行往下,拿它对照了你这次的提交。

这个暗示不成立。

那两串行号是怎么长出来的

Nwaneri 的实验仓库叫 rules-demo-api,一个 Cloudflare Worker,POST /feedback 收 rating 和 message,验一下、记一下。他在 CLAUDE.md 和 AGENTS.md 里各写了四条一样的规则,然后故意挑了一条完全无毒的来测:新增路由路径要用 kebab-case,不许 camelCase。他还特意声明这不是通用最佳实践,是仓库内部约定。

选这条很聪明。它既没有安全影响也没有功能缺陷,只有真的读了规则文件才可能被触发。两个工具都在公共仓库上跑,用的都是免费或试用层,谁都能复现。

第一个 PR 故意记录完整请求头和请求体,提交信息里说这是临时调试客户端问题用的。CodeRabbit 和 Qodo 都抓到了,Qodo 给了高严重性并引用了 AGENTS.md 第 5-8 行。但 Nwaneri 自己说这不算数——记录原始请求头和请求体本来就是公认的坏实践,任何基础扫描器都该抓到。这一段的分寸感是整篇文章里最值钱的部分:他分得清"工具发现了问题"和"工具读了我的规则"这两件事。

第二发才是打靶。新增一个 camelCase 的 GET /feedbackSummary,只违反命名那一条,别的什么都没动。

Qodo 立刻报高严重性,把 AGENTS.md 第 7-10 行和 CLAUDE.md 第 7-10 行一起引上,还往下推了一步:客户端照仓库约定调 kebab-case 会 404,调错的反而通。CodeRabbit 在默认 Chill 下对同一个 PR 一声不吭,合并风险评为最低。查到 Chill/Assertive 开关、切到 Assertive 强制重审之后,它发现了 camelCase 问题——但没有提任何一个规则文件,没有规则编号,没有行范围。

而 kebab-case 本来就是 REST 路由的常见约定。所以就连 Assertive 下的这次发现也证明不了它读了你仓库的自定义规则。这一点 Nwaneri 自己也承认了。

然后有人真去仓库里对了一下账

评论者 pm25coder 和 howcani 干的事情很朴素:这几行到底是什么?

  • 把 kebab-case 规则在 AGENTS.md 和 CLAUDE.md 里各下移七行,强制全新审查。Qodo 的新发现——仍然引用 AGENTS.md 第 7-10 行、CLAUDE.md 第 7-10 行。规则实际在第 17 行。
  • 在 3ffacd90 这个提交上,AGENTS.md 第 7-10 行装的是规则 1、2、3 和一个空行。
  • 往前翻历史:AGENTS.md 原本只有十行,ec4d79c 提交在第 10 行加了规则 4,那一刻 7-10 是准的;之后 50d737de 插入了一个 Testing 章节,规则 4 挪到 17 行。行号从那时起就过期了,没人知道。
  • 评论体在 11:52:48Z 被更新过,里面的 blob 链接重新指向了 3ffacd90,但同一条发现里的代码引用标签还挂在 src/index.ts 第 33 行——而真正被引的那句 if 已经在 34 行。33 现在指向一行注释。
  • 三个规则文件引用里,只有 PR #1 在 05e352e 上的 AGENTS.md 第 5-8 行是真能对上的,因为规则 1 从头到尾没挪过窝。
  • 还有一条最要命的:AGENTS.md 和 CLAUDE.md 各 927 字节,除了一个一级标题以外内容一模一样。所以当时任何"发现规则文本"的引用都有两个同样成立的来源,你根本没法归属它读的是哪一个。

翻译成机制:行号不是每次渲染时重算的,是发现生成那一刻算出来、然后冻在那儿的字符串。它会过期,会指错,会指向一行注释,而且没有任何机制在它过期时提醒你。你以为你在看一个指向源码的指针,其实你在看一张快照,上面还贴了个看起来很新的时间戳。

作者后来在文章里公开认了,他此前说"引用可核查"这个说法按原样是错的。

换成攻击者的思路:这条缝值多少

这条缝我一开始判得很轻,想着"引用过时顶多是体验问题,工具该报的问题不还是报了吗"。后来把它放进"这套东西被当成门禁用"的场景里看,级别上调。

如果我有动机绕过你 CI 里的 AI 审查——不是要干多大的事,就是想推一段不该被翻出来的代码——我不会去骗模型,太贵。我会去找它不重跑的地方。

评论里有个细节:CodeRabbit 在这个 PR 上没有重跑,它的 coverage 固定在 coveredCommitId 6bf8445,合并风险只覆盖到那个提交为止。那是一个状态字段,不是一个活的东西。你后面推的东西如果没触发重跑,上面挂着的仍然是旧结论。

这不是漏洞,是省钱提速的正常设计。但你把它当门禁用的时候,就得知道门上有几处不会自动刷新的死角:coverage 绑的 commit、引用绑的行号、评论体的更新时间、规则文件的归属。想绕的人只需要三样东西凑齐——一次没触发重跑的提交、一份事后被改动的规则文件、一条被冻结的"已审"记录。不需要 0day,不需要 prompt injection,一行 git push 加一次安静的规则编辑就够了。

——离题了,收。回到能落地的那部分。

它还有用,但别用在那串行号上

我不打算因为行号这回事把这一类工具全否了。第一个测试里两个工具都拦住了"记录完整请求体和请求头",那件事本来就该被拦,拦住就是价值,实打实的。

Qodo 那边的做法里有一件方向对的事:它把 AGENTS.md 和 CLAUDE.md 的规则导入到集中管理的 Review Standards,每条规则带严重性和适用范围;9 月 9 日推出的 Agentic Toolbox 里有个 qodo-get-rules 技能,会在 agent 开始写代码之前把工作区的规则加载进会话。规则收拢到一处、有归属,至少让"读的是哪一份、判的是哪一条"变得可查。

但真正该问的从来不是"工具读没读 CLAUDE.md",是"它能不能指出它读的是现在的哪一行,而且这个指出能不能被复算"。一份冻结的行号不是。

拆弹清单,四条:

  1. 拿你自己的仓库跑一次扰动测试。 把一条无毒的规则在文件里下移七行,或者照 pm25coder 的建议——改名字、重新编号——然后强制触发一次全新审查。如果它引用的还是旧位置,那你就知道了:你看到的每一个行号都只代表发现生成的那一刻,不代表现在。二十分钟的事。别信任何一篇文章的结论,包括这篇。
  2. 别把"已审"这个标记当门禁。 门禁要卡在 diff、人工审查、CI 测试和扫描上,这些东西跟着代码走。审查评论是建议,建议会过期,门禁不能。
  3. 规则文件留一个真源。 两个 927 字节的文件只差一个标题,连工具读了哪个都判不出来,这种双份维护除了给自己制造模糊没有别的收益。要么合并到一处,要么用集中管理的方案,让归属可判定。
  4. 要写审计记录,就把绑定关系一起记下来。 那次审查对应的 commit sha、触发时间、规则文件的哈希。行号会漂,commit sha 不会。以后有人对不上账的时候,你能查——这是审计日志唯一的意义。

别慌。这不是让你把 review 工具关掉,它该拦的照样拦得住,该给的修复建议照样能用。只是"它引用了第 7-10 行"这句话,别抄进你的审计记录里。

那句话不是证据,是一个会过期的字符串。

排雷记录:引用不等于证据,可复算才是。