← 返回博客

    每份 CLAUDE.md 都该通过的 33 项 checks

    当我们开始大规模整理 CLAUDE.md 的失败模式时,本来以为会找到十个或十五个。最后是三十三个。它们都有同一个特点:每一个都是具体、可证伪的问题,作者大概率没注意到,agent 则悄悄绕过去了。这篇会按它们造成的伤害类型,把 AgentLint 跑的三十三项 checks 分组讲清楚,也解释为什么每一项都值得留下。

    为什么是三十三,不是五个

    linter 的第一个版本只有九项 checks。我们以为够了。然后我们拿它去跑接下来能找到的三十份 CLAUDE.md:开源 repo、我们自己的内部项目、贡献者发来的文件。结果同样的模式不断出现,原来的九项也抓不住。模糊规则穿着具体规则的衣服。具体规则被自己的例子打脸。章节标题承诺一件事,内容却在做另一件事。等目录稳定下来时,它已经有五个维度、三十三项 checks。不是因为三十三是魔法数字,而是因为继续加新 check 已经抓不到旧 check 抓不到的东西了。

    这五个维度按造成痛苦的程度排序是:正确性具体性结构执行卫生。大多数 CLAUDE.md 先死在具体性上。少数在正确性上坏到 agent 直接不信这个文件。

    正确性 checks(33 项里的 8 项)

    这些 checks 问的是:这个文件说的,和项目现在真实存在的样子一致吗?

    第一项是 过期 path 引用:CLAUDE.md 里提到的每个 file path 都必须能解析。如果文件说 “see scripts/deploy.sh”,那 scripts/deploy.sh 就必须存在。这个 check 本质上是一行:grep 所有被引用的 path,然后 shell out 到 test -e。它抓的是最尴尬的漂移:规则还在引用六个 commit 前已经删掉的文件。

    第二项是 过期 command 引用:提到的每个 shell command 都必须能跑。这个 check 会验证 syntax(shellcheck 能不能 parse),能做的话还会跑 --help 确认 binary 存在。像 “run bun test” 这种规则,在项目迁到 npm 后会悄悄坏掉。

    第三项是 文件内部规则互相冲突:一条明确禁止另一条明确要求的事情。checker 会在像规则的句子上用一个小 NLI model,报出高置信冲突。最常见模式:前面说“所有改动都走 PR”,后面说“小修可以直接进 main”。

    第四项是 CLAUDE.md 和 README 冲突:同一个项目,在两个文件里写了两套说明。CLAUDE.md 说 build command 是 X,README 说是 Y。其中一个是错的。这个 check 会拆开两个文件,标准化 claim,然后报冲突。

    第五项是 CLAUDE.md 和 CI 冲突:规则说它应该由 CI 执行,但 CI 文件实际上没有执行。“Tests must pass before merge” 可是 GitHub Actions workflow 里没有 test step。

    第六项是 语言 pinning:如果项目用 TypeScript,文件就该写出来。如果项目用 Python 加 uv,文件就该写 uv 而不是 pip。这个 check 会 parse package manager files(package.json、pyproject.toml、Cargo.toml),再和规则交叉比对。

    第七项是 过时 tooling:规则提到了项目已经迁走的工具。比如一个已经切到 bun workspaces 的项目里还写着 “Use lerna run”。这个 check 有一个小 registry,记录常见迁移,并报出可疑情况。

    第八项是 默认 branch 错误:规则写 “master”,但 repo 默认是 “main”,或者反过来。

    具体性 checks(33 项里的 10 项)

    这些 checks 看规则是否具体到 agent 能执行。

    Hedge-word 密度:每百词里 “should”、“may”、“consider”、“if appropriate” 这类词的数量。超过阈值后,文件就更像建议书,不像指令。修法是把 hedge 改成具体规则。

    不可证伪的说法:像 “write good code” 或 “be careful” 这种规则。check 会报任何动词太抽象、没法验证的 imperative(good、careful、clean、maintainable、robust、proper)。

    缺少阈值:提了质量,但没有量化。“Tests should be fast” 却没有时间预算。“Files shouldn't be too long” 却没有行数限制。

    无法验证的说法:引用 agent 的意图(“understand the code”)或感受(“feel confident”)。agent 两者都没法验证。

    同义词堆叠:一条规则里三个或更多近义词(“clear, concise, readable”)。字变多了,具体性没变多。

    没有范围的 “always” / “never”:绝对规则没有例外说明,但 codebase 明显有例外。

    缺少例子:那些如果加一个具体例子,就能把规则压短一半、合规率翻倍的规则。

    模糊引用 “best practices”:只有指针,没有目标。“Follow JavaScript best practices” 是谁的 best practices,哪一年,哪份文件?

    愿望型规则:描述团队希望为真的东西,而不是实际为真的东西。“We always write tests first.” 如果你们没有这么做,这条规则就是谎言。

    没有理由的一刀切禁止:禁止某件事,但没解释为什么。agent 会遵守到它发现一个看起来违反意图的 edge case。没有 rationale 时,它只能猜。

    结构 checks(33 项里的 6 项)

    这些 checks 问的是:这个文件的形状够不够清楚,agent 能不能便宜地导航它。

    章节长度上限:单个 section 超过可配置预算(默认 80 行)。长 section 会被人和 agent 一起跳读。

    总长度上限:整个文件的长度。默认 300 行。AgentLint 到上限会 warn,远超会 error。

    必需章节:文件必须包含的 section headings,可配置。默认是 blog 01 里的五个章节:project intro、how-we-work、language/style、operational notes、principles。

    标题层级:H1 → H2 → H3,不能跳级。常见漂移:H1 后面直接 H4,因为有人 copy-paste 了一段 snippet。

    重复标题:同一个 heading 出现两次,通常表示两个人没看到对方的 edit,分别加了同一节。

    孤儿段落:不属于任何 section 的 text。check 会报第一条 heading 前,或最后一条 heading 后那些不归属任何章节的段落。

    执行 checks(33 项里的 5 项)

    这些 checks 问的是:那些号称要机器执行的规则,执行层真的存在吗?

    Pre-commit hook coverage:规则说 “commit 前跑 lint”,就需要 .husky/pre-commit 或等价物真的跑 lint。

    CI step coverage:规则说 “tests must pass”,就需要一个 CI step 跑它们,并且有 required-status protection。

    Lockfile rule coverage:如果文件说“不要手改 lockfiles”,就应该有 CI guard 或 pre-commit,阻止 npm install 之外的 lockfile edits。

    Secret-scanning rule coverage:如果文件说 “no secrets in commits”,就应该有 .gitignore 覆盖 .env*,并在 CI 里有 secret scanner。

    License-header rule coverage:如果文件要求 source files 里有 license headers,check 会验证一定比例的 source files 真的有。

    卫生 checks(33 项里的 4 项)

    这是最小的维度,也是 false-positive 最低的一组。

    TODO 没有 owner 或 date:CLAUDE.md 里的 TODO comments 如果没有名字或日期,最后会变成永久 TODO。

    Trailing whitespace 和混合 line endings:小问题,但会慢慢腐蚀。

    Fence style 不一致:triple-backtick code fences 的 language tags 或 fence widths 不一致。

    过期的 “as of” 日期:带日期戳的规则(“as of 2024-12”)超过六个月没有碰过。

    一个完整例子:一个文件,十一个失败

    我们拿 linter 跑过一个开源 repo,里面有 240 行 CLAUDE.md。报告回来有十一个不同发现。三个是正确性(一个过期 path,两个过期 commands),四个是具体性(三组 hedge clusters 和一个不可证伪 claim),两个是结构(一个 section 超过长度上限,一个 heading hierarchy 跳级),一个是执行(“tests must pass” 规则没有 CI step),一个是卫生(过期的 “as of 2024-09” stamp)。维护者修完十一个问题大约花了四十分钟,并从文件里删掉了大约六十行。下一周,用一个小 benchmark 看 agent 完成 PR 的表现,项目上的 agent 行为明显更一致。

    linter 的目标不是给 CLAUDE.md 打分。它是为了在 agent 必须穿过这些问题之前,先移除最常见的失败模式。通过全部三十三项 checks 的文件,不一定就是很棒的 CLAUDE.md。但它不太可能悄悄误导 agent。这就是底线。

    目录接下来会去哪里

    三十三是现在的数字,但我们会随着新模式出现继续加 checks。接下来正在测试的两项,一个围绕 AGENTS.md(它正在变成多工具版 CLAUDE.md 的继任者),一个围绕 .cursor/rules 文件(Cursor 的 rule-loading semantics 有自己的失败模式)。如果你发现 AgentLint 没抓到的模式,而且你觉得它应该抓,贡献路径很简单:一个 issue,一个 rule definition,一个 test fixture。这个目录靠遇到真实世界里的文件长大。我们跑得越多,它就越好。

    相关文章