跳到正文
Hacker News· fitzyracing1·· 10 天前AI 评分62

Fakegreen 发布:不用 LLM 检测 AI 编码智能体伪造绿色构建

Fakegreen: Catch AI coding agents faking a green build (no LLM)

AI 导读

Fakegreen 是一个无需 LLM 和 API key 的命令行工具,通过读取 git diff 识别 AI 编码智能体伪造测试通过的行为,通常在 100 毫秒内完成,可用 npx fakegreen 直接运行。

正文 · AI 翻译

npm License: MIT Node.js

一条命令扫描你的 agent 的 diff,并标记出它伪造绿色构建的每一种方式。

适用对象

  • 使用 Claude Code、Codex、Cursor、Gemini CLI 或 Aider 交付的团队,希望对假绿 diff 进行硬性拦截
  • 希望拥有确定性 CI / pre-commit / 回合结束触发线的维护者——无需 LLM、无需 API key、零运行时依赖
  • 任何厌倦了 agent .skip 测试、弱化断言或追加 || true 然后声称 "All tests pass ✅" 的人

⭐ 如果 fakegreen 为你捕获了一个假绿提交,给仓库点个 star——这能帮助其他团队发现这个触发线。

30 秒内试用

npx fakegreen

无需安装。需要 Node.js 18+ 以及你的 PATH 上的 git。准备好后用 npm i -D fakegreen 固定版本。

LLM 代码审查 fakegreen
速度与成本 数秒到数分钟,API 开销 通常 低于 100 ms,免费,离线
确定性 可能变化;容易被绕开 每次答案相同
设置 API key + 提示词 零运行时依赖——npx fakegreen

编码 agent(Claude Code、Codex、Cursor、Gemini CLI、Aider)因"测试通过"而获得奖励。有时它们通过删除测试、给它加上 .skip、把 toBe(42) 换成 toBeDefined()、添加 @ts-ignore、教代码检测 NODE_ENV === 'test',或在 CI 步骤中追加 || true 来达到目的。然后它们说 "All tests pass ✅"。

fakegreen 读取 git diff 并捕获这些操作。它是确定性的,不需要 LLM 和 API key,具有零运行时依赖,通常能在低于 100 ms 内完成。将它挂接到你的 agent 的回合结束事件,这样 agent 在能够声称完成之前就会被阻止并被告知它做了什么。

fakegreen catching a fake-green agent commit

演示

上面的录制(docs/demo.cast、docs/demo.gif、docs/demo.png)是对由 scripts/make-demo-repo.sh 构建的示例仓库的真实运行。一个诚实的提交添加了一个带测试的购物车模块。然后一个标题为 fix: make the test suite pass 的"agent"提交跳过一个测试、弱化一个断言、删除一个测试文件、添加 @ts-ignore 以及一个 NODE_ENV === 'test' 捷径,并向 CI 追加 || true:

$ fakegreen --last-commit
fakegreen · last commit (8797f43) · 4/4 files · +5 −13

   HIGH  .github/workflows/ci.yml:10  ci-failure-ignored
         Failure ignored with `|| true` on a test/check command
         │ + - run: npm test || true

   MED   src/cart.ts:4  suppression-added
         Checker silenced with @ts-ignore
         │ + // @ts-ignore

   HIGH  src/cart.ts:5  test-env-special-case
         Non-test code branches on the test env (NODE_ENV === 'test'); tests skip the real path
         │ + if (process.env.NODE_ENV === 'test') return items.length ? 8 : 0;

   HIGH  test/cart.test.ts:9  test-skipped
         Test skipped with .skip
         │ + it.skip('applies SAVE10', () => {

   HIGH  test/cart.test.ts:14  assertion-weakened
         Specific assertion replaced with a vague one
         │ - expect(total([])).toBe(0);
         │ + expect(total([])).toBeDefined();

   HIGH  test/checkout.test.ts  test-file-deleted
         Test file deleted (2 test cases removed)
         │ - it('rejects negative quantities', () => {

  5 high · 1 medium · 0 low  ✖ fake green detected (fail-on: high) · 41ms

退出码 1。用 asciinema play docs/demo.cast 重放它,或用 scripts/make-demo-repo.sh /tmp/fakegreen-demo && cd /tmp/fakegreen-demo && fakegreen --last-commit 重建它。

安装

npx fakegreen                 # run once, no install
npm i -D fakegreen            # or pin it in a project

你需要 Node.js 18 或更新版本,以及你的 PATH 上的 git。

用法

fakegreen                     # uncommitted + staged changes vs HEAD, plus untracked files (default)
fakegreen --staged            # only what is staged (good for pre-commit)
fakegreen --base origin/main  # everything since the merge-base with main, including uncommitted work (good for PRs)
fakegreen --last-commit       # just HEAD~1..HEAD
fakegreen --commit <sha>      # one specific commit
git diff main... | fakegreen --diff -   # any unified diff from stdin or a file

fakegreen --json              # machine-readable findings
fakegreen --sarif > fg.sarif  # SARIF 2.1.0 for GitHub code scanning and IDEs
fakegreen --format github     # ::error annotations for GitHub Actions
fakegreen --fail-on medium    # exit 1 on medium or higher (default: high; `none` never fails)
fakegreen --min-severity medium  # hide low-severity findings
fakegreen rules               # list every rule

退出码:0 干净(或低于 --fail-on),1 发现的问题达到或超过 --fail-on,2 用法或 git 错误。

每个发现都包含严重程度、file:line、规则 id、简短说明,以及违规的 +/- 行。--json 输出如下所示:

{
  "tool": "fakegreen", "version": "0.1.0", "source": "last commit (8797f43)", "failed": true,
  "summary": { "high": 5, "medium": 1, "low": 0, "total": 6, "files": 4, "analyzedFiles": 4, "added": 5, "removed": 13 },
  "findings": [
    { "ruleId": "test-skipped", "severity": "high", "file": "test/cart.test.ts", "side": "added", "line": 9,
      "message": "Test skipped with .skip", "snippet": "+ it.skip('applies SAVE10', () => {" }
  ]
}

检查内容

它只查看 diff 中新增和删除的行,因此旧代码不会被重新标记。注释和字符串字面量在匹配前会被词法分析剔除,这可以防止字符串或注释中的 ".skip" 触发任何内容。仅被移动的行(重命名、文件拆分、重新排序)会被忽略。被移动的测试文件,或其测试出现在另一个文件中的测试文件,不算作删除。支持的语言:JavaScript/TypeScript、Python、Go、Rust、Java(外加 Kotlin 测试注解),以及 CI 和配置文件:GitHub Actions、GitLab CI、package.json、tsconfig、jest/vitest/c8/nyc 配置、pyproject.toml/setup.cfg/tox.ini/.coveragerc、Maven/Gradle、Makefile,以及 husky/shell 钩子。

规则 默认 捕获内容
test-file-deleted 高 测试文件被删除(且未在 diff 中其他位置移动/重命名)。
test-count-dropped 高 测试用例(test()/it()/def test_/func Test/#[test]/@Test)的净数量减少。
test-skipped 高 添加了跳过:.skip、xit/xdescribe、@pytest.mark.skip/xfail、pytest.skip()、t.Skip()、#[ignore]、@Disabled、@Ignore。
test-focused 高 .only / fit / fdescribe 会静默禁用文件或运行中的其他所有测试。
test-conditional-skip 低 添加了条件跳过(skipif、skipIf、importorskip、assumptions、if testing.Short())。
assertion-removed 中 测试文件中的净断言数量下降(expect/assert/t.Error/assert_eq!/assertEquals...)。
assertion-weakened 高 某个具体断言(toBe/toEqual/assert x == y/assertEquals)被替换为模糊断言(toBeTruthy/toBeDefined/assert x/assertNotNull)。
assertion-trivial 高 添加了永远不会失败的断言(expect(true).toBe(true)、assert True、assert!(true))。
assertion-expected-changed 低 仅断言的预期字面值发生了变化。确认是代码有误,而非测试有误。
suppression-added 中 @ts-ignore、@ts-nocheck、@ts-expect-error、eslint-disable、# type: ignore、noqa、//nolint、#[allow(...)]、@SuppressWarnings 及其同类。全局抑制为中,指定具体规则的为低,文件/包级别的为高。
coverage-exclusion-added 低 istanbul/c8/v8 ignore、pragma: no cover、LCOV_EXCL、#[coverage(off)]。
ci-failure-ignored 高 || true、continue-on-error: true、allow_failure: true、--exit-zero、在测试/lint/构建命令上使用 set +e。
ci-step-removed 高 从 CI 配置、脚本或 package.json 中移除了运行测试、lint 或类型检查的命令。
ci-step-disabled 高 用 if: false 或 when: never 禁用了某个 CI 作业/步骤。
test-script-neutered 高 package.json 的 test 脚本被替换为 echo/true/exit 0。
test-exclusion-added 中 --passWithNoTests、-DskipTests、-x test、testPathIgnorePatterns、--ignore/--deselect、collect_ignore。
coverage-threshold-lowered 高 覆盖率阈值(coverageThreshold、fail_under、--cov-fail-under、thresholds、jacoco minimum...)被降低或移除。
typecheck-weakened 中 tsconfig 严格标志被关闭,mypy ignore_errors / strict = false,pyright typeCheckingMode off。
lint-rule-disabled 低 在 ESLint 配置中将某条 lint 规则切换为 "off"/0。
test-env-special-case 高 非测试源代码根据是否处于测试环境(NODE_ENV === "test"、JEST_WORKER_ID、sys.modules 中的 "pytest"、testing.Testing()、cfg!(test))或是否处于 CI 而分支。
error-swallowed 中 新增空的 catch / except: pass / .catch(() => {}) / if err != nil {}。
ignore-comment-added 低 添加了内联的 fakegreen-ignore 注释。始终报告,以便审查者看到被豁免的内容。

严重程度在关键处进行分级。例如,全局的 # type: ignore 为中,针对性的 # type: ignore[attr-defined] 为低,而文件级别的 // @ts-nocheck 为高。|| true 作用于 npm test 为高,而 || true 作用于 rm -rf build 为低。条件性的 skipif(sys.platform == "win32") 为低,但 skipif(True) 为高。

自动跳过:Markdown/文档、锁文件、vendored 和 node_modules 代码、生成的文件,以及 fixtures/ / testdata/ 目录。

让 agent 立即停下:回合结束钩子

Claude Code、Codex 和 Gemini CLI 可以在 agent 结束回合时运行命令并阻止停止,将 命令的反馈发送回模型。fakegreen hook 会说出每个 agent 的协议。如果发现达到或 超过 --fail-on 的问题,agent 会被告知它具体伪造了什么,并被要求修复,或者停下来向你解释为什么 这一改动是有意为之。

npx fakegreen install claude     # .claude/settings.json   (--local → settings.local.json, --global → ~/.claude)
npx fakegreen install codex      # .codex/hooks.json       (--global → ~/.codex/hooks.json)
npx fakegreen install gemini     # .gemini/settings.json   (--global → ~/.gemini/settings.json)

安装程序会显示配置变更的 diff,并在写入前询问。传入 --yes 可跳过提示,或 传入 --dry-run 仅预览。它会合并到现有配置中,是幂等的,并拒绝触碰不是 有效 JSON 的文件。

Claude Code:.claude/settings.json

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "npx --yes fakegreen hook --agent claude",
            "timeout": 120,
            "statusMessage": "fakegreen: checking the diff for fake-green changes"
          }
        ]
      }
    ]
  }
}

发现问题时,钩子会打印 {"decision":"block","reason":"..."},Claude 会以该原因作为 下一条指令继续工作。(Claude Code hooks 文档)

Codex:.codex/hooks.json

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "npx --yes fakegreen hook --agent codex",
            "timeout": 120,
            "statusMessage": "fakegreen: checking the diff for fake-green changes"
          }
        ]
      }
    ]
  }
}

发现问题时,钩子会打印 {"decision":"block","reason":"..."},Codex 会以该原因作为新 提示继续。Codex 仅在项目的 .codex/ 层被信任时才加载项目钩子,并且它会要求你在运行每个新钩子 (/hooks)之前审查并信任它。 (Codex hooks 文档)

Gemini CLI:.gemini/settings.json

{
  "hooks": {
    "AfterAgent": [
      {
        "hooks": [
          {
            "name": "fakegreen",
            "type": "command",
            "command": "npx --yes fakegreen hook --agent gemini",
            "timeout": 120000,
            "description": "Block the turn when the diff fakes a green build"
          }
        ]
      }
    ]
  }
}

发现问题时,钩子会打印 {"decision":"deny","reason":"..."},这会让 Gemini 带着原因作为反馈重试该轮次。Gemini 超时以毫秒为单位。(Gemini CLI hooks reference)

Cursor 和 Aider。两者都还没有 fakegreen 所针对的阻断式回合结束钩子。请使用下面的 pre-commit 钩子和 agent skill,或者在接受 agent 的工作之前运行 npx fakegreen --base main。Aider 默认会跳过 git 钩子,所以要么用 --git-commit-verify 启用它们,要么在每次 Aider 提交后运行 npx fakegreen --last-commit。

钩子详情:

  • 干净的 diff 不产生输出。fakegreen 静默退出,返回码为 0。
  • 错误时放行。如果 fakegreen 自身出故障,你的 agent 绝不会被卡住。
  • 循环防护。如果 agent 已经被阻断过一次(stop_hook_active)且发现的问题没有变化,第二次停止会被允许,并向你显示一条警告。这样,已经解释过有意改动的 agent 就不会被困住。
  • 选择 diff。默认情况下,钩子扫描未提交的工作与 HEAD 的对比。如果你的 agent 边做边提交,请改为扫描整个分支:fakegreen install claude --hook-args "--base origin/main"。
  • 在 npm 发布之前,或使用本地检出时:用 --command "node /path/to/fakegreen/dist/cli.js" 直接将钩子指向构建产物。

Pre-commit、CI 和 agent skill

npx fakegreen install pre-commit     # git pre-commit hook (respects core.hooksPath and .husky/pre-commit)
npx fakegreen install github-action  # writes .github/workflows/fakegreen.yml
npx fakegreen install skill          # copies SKILL.md to .claude/skills/fakegreen/ (--global → ~/.claude/skills)

pre-commit 框架(.pre-commit-config.yaml):

- repo: https://github.com/fitzyracing1/fakegreen
  rev: v0.1.0
  hooks:
    - id: fakegreen

GitHub Actions。发现的问题会以注解形式显示在 PR 上。参见 examples/github-workflow.yml:

- uses: actions/checkout@v4
  with: { fetch-depth: 0 }
- uses: fitzyracing1/fakegreen@v0.1.1   # composite action; inputs: version, base, fail-on, args
  with:
    fail-on: high

Agent skill。skills/fakegreen/SKILL.md 告诉 agent 绝不要为了变绿而跳过、删除或弱化测试,绝不要对测试环境做特殊处理,并且在声称完成之前先运行 npx fakegreen。它可作为 Claude Code skill 使用。把它粘贴到 AGENTS.md、GEMINI.md、.cursor/rules 或 CONVENTIONS.md 中以供其他 agent 使用。

配置

在 package.json 中添加可选的 .fakegreenrc.json、.fakegreenrc 或 "fakegreen" 键:

{
  "rules": {
    "coverage-exclusion-added": "off",
    "suppression-added": "low",
    "error-swallowed": "high"
  },
  "ignore": ["scripts/**", "**/generated/**"],
  "testPatterns": ["e2e/**/*.ts"],
  "failOn": "high",
  "untracked": true
}
键 含义
rules 按规则的 "off" 或严重级别覆盖("high"、"medium"、"low")。
ignore 要完全跳过的文件的 glob 模式。
testPatterns 应被视为测试的文件的额外 glob 模式。
failOn --fail-on 的默认值。
untracked 在工作树扫描中包含未跟踪的文件(默认 true)。

允许有意的改动

在该行或上一行添加 fakegreen-ignore 注释。你可以指定规则名,并且加上原因是个好主意:

// fakegreen-ignore test-skipped -- flaky upstream API, tracked in #123
it.skip('talks to the payments sandbox', async () => { ... });
except Exception:  # fakegreen-ignore error-swallowed: best-effort telemetry
    pass

对于被删除的行(比如删除的断言),把注释放在同一 hunk 中任意新增行处。fakegreen-ignore-file 可豁免整个文件。每条忽略注释本身都会作为 low 级别的发现被报告(ignore-comment-added),因此审查者仍能看到被豁免了什么。钩子的反馈会明确告诉 agent 不要自行添加豁免。如果你完全不信任 agent 处理这些,请设置 "rules": { "ignore-comment-added": "high" },这样任何新的豁免都会阻断该轮次并呈现给你。

FAQ

为什么不直接让 LLM 来审查 diff? LLM 审查者速度慢、花钱、需要密钥,而且容易被说服绕过。fakegreen 在毫秒内运行,每次给出相同答案,并且可离线及在 CI 中工作。如果你愿意,两者都用。fakegreen 是每轮都运行的廉价绊线。

它会不会误报我正常的重构? 有时会,而这正是审查信号的意义所在。移动和重命名会被识别,跨文件移动的测试以及从一个 CI 命令迁移到另一个的迁移也会被识别。在对 12 个热门人工维护仓库(express、zod、vite、requests、flask、httpx、gin、cobra、ripgrep、clap、gson、spring-petclinic;共 1,200 次提交)最近 100 次提交的 dogfood 运行中,出现了 34 个高严重度发现,大约每 35 次提交一个。几乎全都是真实的测试删除(回滚、功能移除),这些本来就是审查者想看到的。中度和低度发现只是背景信息。默认的 --fail-on high 只会对高严重度发现进行拦截。

它能理解我的代码(AST)吗? 不能。它使用一个小型词法分析器(处理字符串和注释)加上针对变更行的定向模式匹配。这正是它保持快速、无依赖且多语言的原因。请参阅下面的局限性。

agent 能直接禁用 fakegreen 吗? 它可以编辑 hook 配置,但该编辑会出现在你的 diff 中。将 hook 与 pre-commit hook 或 GitHub Action 结合使用,并把对 .claude/、.codex/、.gemini/ 或 .fakegreenrc.json 的更改视为对 CI 的更改。

它会把我的代码发送到任何地方吗? 不会。它从不发起网络调用。它只会调用 git,除此之外什么都不做。

已知局限性

  • 基于行的启发式分析,没有 AST。不寻常的格式(测试声明跨多行、包含引号的正则字面量、宏)可能导致漏报或偶尔的误报。
  • 测试数量补偿是 diff 全局的。如果 agent 删除一个测试并添加一个不相关的琐碎测试,数量不会下降,不过被削弱或琐碎的断言仍会被断言规则捕获。
  • 仅分析 JS/TS、Python、Go、Rust 和 Java/Kotlin 源代码。Ruby、C#、C/C++、PHP、Swift 和自定义测试 DSL 不在分析范围内,不过它们的 CI 配置更改仍会被分析。
  • hook 的默认 diff 是未提交的工作与 HEAD 的对比。如果 agent 在会话期间提交,请使用 --hook-args "--base origin/main"。
  • fakegreen-ignore-file 仅当它出现在 diff 的新增行或上下文中时才会被识别。

支持与服务

fakegreen 采用 MIT 许可证,CLI、hooks、pre-commit hook 和 GitHub Action 将保持免费。我是 Joshua Almeida,独自构建和维护它。如果它帮你避免合并一个实为被删除的“已修复”测试套件,这里有三种提供帮助的方式:

赞助该项目。 GitHub Sponsors 为我投入在新规则、新语言和误报修复上的时间提供资金。每一份赞助都有帮助,小额赞助也包括在内。

fakegreen for Teams(早期访问等候名单)。 我正在研究一个面向在多个仓库中运行 AI 编码 agent 的团队的托管版本。计划是做一个 GitHub App,将发现作为 PR 评论和检查运行发布,为整个组织提供一套共享策略,并按 agent 和仓库分组记录假绿事件的历史。 目前什么都还没构建。 如果你的团队会使用它,请加入等候名单并告诉我你需要什么,因为这将决定构建什么。

咨询与自定义规则。 如果你的团队正在推广 Claude Code、Codex、Gemini CLI、Cursor 或 Aider,我可以帮你为自己的代码库、测试约定和 CI 设置编写 fakegreen 规则,并搭建 agent 无法悄悄绕过的护栏(hooks、pre-commit、CI 门禁)。请发送电子邮件至 fitzyracing1@gmail.com。

贡献

参见 CONTRIBUTING.md。新的检测器需要一个真阳性测试用例以及一个假阳性防护。

许可证

MIT © 2026 Joshua Almeida

来源:Hacker News · github.com