Fakegreen 发布:不用 LLM 检测 AI 编码智能体伪造绿色构建
Fakegreen: Catch AI coding agents faking a green build (no LLM)
Fakegreen 是一个无需 LLM 和 API key 的命令行工具,通过读取 git diff 识别 AI 编码智能体伪造测试通过的行为,通常在 100 毫秒内完成,可用 npx fakegreen 直接运行。
一条命令扫描你的 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 在能够声称完成之前就会被阻止并被告知它做了什么。
演示
上面的录制(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
