원문 캡처 · github.com
원문 캡처 · github.com

fakegreen의 시연용 저장소에서 에이전트는 테스트 하나를 건너뛰고, 단언을 약화하고, 테스트 파일을 삭제했다. 코드 검사 무시 지시문을 넣고 테스트 환경에서만 동작하는 분기도 추가했다. CI 검사 명령에는 || true를 붙였다. 도구는 이 변경을 찾아냈다.

Joshua Almeida가 공개한 fakegreen은 AI 코딩 에이전트가 테스트를 무력화하거나 검사 실패를 숨기는 변경을 찾아내는 명령줄 도구다. Claude Code, Codex, Cursor, Gemini CLI, Aider를 쓰는 팀이 에이전트의 변경 사항을 검사하고, 문제를 발견하면 작업을 막는 데 쓸 수 있다.

fakegreen은 무엇을 검사하나?

코드 변경 비교(diff)는 바뀐 줄을 이전 버전과 견주는 기록에 빗대면 이해하기 쉽다. fakegreen은 이 기록에서 추가되거나 삭제된 줄을 살펴 테스트 생략, 검증 약화, CI 검사 제거 같은 변경을 찾는다. 언어 모델(LLM)이나 API 키가 필요하지 않으며, 네트워크를 호출하지 않는다.

시연은 scripts/make-demo-repo.sh가 만든 예제 저장소에서 실행한 것이다. 정상적인 변경으로 테스트가 있는 카트 모듈을 추가한 뒤, 가상의 에이전트가 테스트를 무력화하는 변경을 넣었다. 아래는 fakegreen의 출력이다.

text
$ 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

이 실행은 41ms가 걸렸고, 높은 심각도 발견 사항이 있어 종료 코드 1을 냈다. 시연을 다시 재생하거나 저장소를 다시 만들 수 있다.

text
asciinema play docs/demo.cast
scripts/make-demo-repo.sh /tmp/fakegreen-demo && cd /tmp/fakegreen-demo && fakegreen --last-commit

어떤 변경을 찾아내나?

fakegreen은 줄을 옮기거나 파일을 나누는 것만으로는 다시 경고하지 않도록 이동을 무시한다. 주석과 문자열은 검사 전에 구문을 구분해 걸러내므로, 문자열 안의 .skip 같은 글자가 규칙을 작동시키지 않게 한다. 테스트 파일이 다른 곳으로 이동했거나 테스트가 다른 파일에 다시 나타난 경우 삭제로 세지 않는다.

규칙기본 심각도찾아내는 변경
test-file-deleted높음다른 곳으로 옮기지 않은 테스트 파일 삭제
test-count-dropped높음테스트 케이스 순개수 감소
test-skipped높음.skip, pytest.skip() 등으로 테스트 건너뛰기
test-focused높음.only, fit, fdescribe로 다른 테스트 비활성화
test-conditional-skip낮음조건부 테스트 생략 추가
assertion-removed중간테스트 파일의 단언 개수 순감소
assertion-weakened높음구체적인 검증을 모호한 검증으로 교체
assertion-trivial높음실패할 수 없는 단언 추가
assertion-expected-changed낮음단언의 예상값만 변경
suppression-added중간@ts-ignore, noqa, //nolint 같은 검사 억제 추가
coverage-exclusion-added낮음커버리지 제외 지시문 추가
ci-failure-ignored높음검사 명령에 || true, continue-on-error: true 등을 추가
ci-step-removed높음CI 설정·스크립트에서 테스트·린트·형식 검사 명령 제거
ci-step-disabled높음if: false 또는 when: never로 CI 단계 비활성화
test-script-neutered높음package.json의 테스트 명령을 echo, true, exit 0으로 교체
test-exclusion-added중간테스트 제외 옵션 추가
coverage-threshold-lowered높음커버리지 기준을 낮추거나 제거
typecheck-weakened중간형식 검사 설정을 느슨하게 변경
lint-rule-disabled낮음ESLint 규칙을 off 또는 0으로 변경
test-env-special-case높음테스트 환경이나 CI에서만 다르게 동작하도록 실제 코드 변경
error-swallowed중간빈 오류 처리나 오류 무시 코드 추가
ignore-comment-added낮음fakegreen 경고를 무시하는 주석 추가

심각도는 변경의 맥락에 따라 달라진다. 예를 들어 포괄적인 # type: ignore는 중간, 특정 규칙을 지정한 # type: ignore[attr-defined]은 낮음, 파일 전체에 적용되는 // @ts-nocheck는 높음으로 분류한다. 테스트 명령의 || true는 높지만 rm -rf build에 붙인 경우는 낮다. skipif(sys.platform == "win32")는 낮고 skipif(True)는 높다.

JavaScript와 TypeScript, Python, Go, Rust, Java 및 Kotlin 테스트 주석을 분석한다. CI·설정 파일에서는 GitHub Actions, GitLab CI, package.json, tsconfig, jest/vitest/c8/nyc 설정, Python 테스트 설정, Maven·Gradle, Makefile, husky와 셸 훅을 지원한다. Markdown과 문서, 잠금 파일, 외부에서 가져온 코드, node_modules, 생성 파일, fixtures/ 및 testdata/ 디렉터리는 자동으로 건너뛴다.

설치하고 어디에 연결하나?

한 번 실행하려면 Node.js 18 이상과 PATH에서 실행할 수 있는 git이 필요하다. 프로젝트에 고정하려면 개발 의존성으로 설치한다.

text
npx fakegreen
npm i -D fakegreen

검사 범위는 인수로 정한다. 기본 실행은 HEAD 이후의 스테이징·미커밋 변경과 추적되지 않은 파일을 본다.

text
fakegreen
fakegreen --staged
fakegreen --base origin/main
fakegreen --last-commit
fakegreen --commit <sha>
git diff main... | fakegreen --diff -

기계 판독 형식이나 심각도 기준도 지정할 수 있다. 기본적으로 높은 심각도에서 실패하며, none을 지정하면 발견 사항으로 실패하지 않는다.

text
fakegreen --json
fakegreen --sarif > fg.sarif
fakegreen --format github
fakegreen --fail-on medium
fakegreen --min-severity medium
fakegreen rules

각 발견 사항에는 심각도, 파일과 줄 번호, 규칙 ID, 설명, 문제가 된 추가·삭제 줄이 포함된다. 종료 코드 0은 깨끗하거나 지정한 기준보다 낮은 발견 사항만 있다는 뜻이고, 1은 기준 이상의 발견 사항이 있다는 뜻이다. 2는 사용법 또는 Git 오류를 나타낸다. JSON 출력의 예는 다음과 같다.

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', () => {" }
 ]
}

Claude Code, Codex, Gemini CLI에서는 작업 종료 훅에 연결해 발견 사항이 있을 때 에이전트가 멈추도록 할 수 있다. 훅은 에이전트별 설정에 명령을 추가하며, 설치 프로그램은 설정 변경 내용을 보여주고 파일 쓰기 전에 동의를 묻는다. --yes로 확인을 건너뛰거나 --dry-run으로 변경을 미리 볼 수 있다. 기존 설정에 합치며, 반복 실행해도 중복 추가하지 않고 유효하지 않은 JSON 파일은 수정하지 않는다.

text
npx fakegreen install claude
npx fakegreen install codex
npx fakegreen install gemini

Claude Code 설정 예시는 다음과 같다.

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 Code는 그 이유를 다음 지시로 받아 작업을 계속한다. 자세한 내용은 Claude Code 훅 문서에 있다.

Codex 설정은 .codex/hooks.json에 둔다.

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

발견 사항이 있으면 Codex에도 같은 차단 응답을 보낸다. 다만 Codex는 프로젝트의 .codex/ 설정을 신뢰하도록 지정해야 하며, 새 훅을 실행하기 전에 검토와 신뢰 승인을 요청한다. Codex 훅 문서를 참고할 수 있다.

Gemini CLI 설정은 .gemini/settings.json에 둔다.

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"
 }
 ]
 }
 ]
 }
}

Gemini에서는 발견 사항이 있을 때 {"decision":"deny","reason":"..."}를 출력해 해당 이유를 반영한 재시도를 유도한다. Gemini의 시간 제한 단위는 밀리초다. 설정은 Gemini CLI 훅 안내에 있다.

Cursor와 Aider에는 fakegreen이 대상으로 삼는 차단형 작업 종료 훅이 없다. 이들에서는 사전 커밋 훅이나 에이전트 스킬을 쓰거나, 작업을 받아들이기 전에 npx fakegreen --base main을 실행할 수 있다. Aider는 기본적으로 Git 훅을 건너뛰므로 --git-commit-verify를 켜거나 커밋마다 npx fakegreen --last-commit을 실행해야 한다.

작업 종료 훅은 변경이 깨끗하면 아무것도 출력하지 않고 종료 코드 0으로 끝난다. fakegreen 자체에 오류가 나면 에이전트가 막히지 않도록 허용하는 방식으로 실패한다. 이미 한 번 차단한 뒤 발견 사항이 바뀌지 않았다면 반복 차단을 피하고 경고를 보여준다. 기본 검사 범위는 미커밋 변경과 HEAD의 차이다. 에이전트가 작업 중 커밋한다면 다음처럼 브랜치 전체를 검사하도록 설치할 수 있다.

text
fakegreen install claude --hook-args "--base origin/main"

npm 배포 전이거나 로컬 체크아웃을 쓸 때는 빌드 파일을 직접 지정한다.

text
--command "node /path/to/fakegreen/dist/cli.js"

사전 커밋 훅, GitHub Actions, 에이전트 스킬도 설치할 수 있다.

text
npx fakegreen install pre-commit
npx fakegreen install github-action
npx fakegreen install skill

사전 커밋 프레임워크 설정은 다음과 같다.

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

GitHub Actions에서는 풀 리퀘스트에 발견 사항을 주석으로 표시할 수 있다. 전체 설정 예시는 저장소의 워크플로 파일에 있다.

yaml
- 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

에이전트 스킬인 SKILL.md는 테스트를 생략·삭제·약화하거나 테스트 환경만을 위한 분기를 만들지 말 것, 완료를 선언하기 전에 npx fakegreen을 실행할 것을 지시한다. 다른 에이전트에서는 파일 내용을 AGENTS.md, GEMINI.md, .cursor/rules 또는 CONVENTIONS.md에 넣을 수 있다.

선택적으로 .fakegreenrc.json, .fakegreenrc 또는 package.json의 fakegreen 항목으로 규칙과 검사 대상을 설정한다.

json
{
 "rules": {
 "coverage-exclusion-added": "off",
 "suppression-added": "low",
 "error-swallowed": "high"
 },
 "ignore": ["scripts/**", "**/generated/**"],
 "testPatterns": ["e2e/**/*.ts"],
 "failOn": "high",
 "untracked": true
}

rules는 규칙별로 off 또는 심각도를 지정하고, ignore는 검사에서 제외할 파일 경로를 정한다. testPatterns로 테스트로 취급할 경로를 더할 수 있으며, failOn은 기본 실패 기준, untracked는 작업 트리 검사에서 추적되지 않은 파일을 포함할지 정한다. 기본값은 true다.

의도적인 변경을 허용하려면 해당 줄이나 바로 위에 fakegreen-ignore 주석을 붙인다. 규칙 이름과 사유를 함께 적을 수 있다.

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

삭제된 단언처럼 삭제 줄에 주석을 달 수 없다면 같은 변경 구역에서 추가된 줄에 주석을 둔다. fakegreen-ignore-file은 파일 전체를 제외한다. 무시 주석도 ignore-comment-added라는 낮은 심각도 발견 사항으로 보고된다. 이를 에이전트가 추가하는 것까지 막으려면 해당 규칙을 높은 심각도로 설정할 수 있다.

json
"rules": { "ignore-comment-added": "high" }

어디까지 믿을 수 있나?

Almeida는 사람 손으로 관리한 저장소 12곳의 최근 커밋 100개씩, 총 1,200개를 시험했다고 밝혔다. 이 과정에서 높은 심각도 발견 사항 34건이 나왔다. 약 35개 커밋마다 한 건꼴이며, 대부분 실제 테스트 삭제였다. 되돌리기나 기능 제거에 따른 삭제도 포함됐고, Almeida는 검토할 만한 신호라고 설명한다. 중간·낮은 심각도의 발견 사항은 맥락을 살펴야 한다. 기본 설정은 높은 심각도에서만 차단한다.

이 도구는 추상 구문 트리(AST)를 분석하지 않는다. 문자열과 주석을 구분하는 작은 어휘 분석기와 변경된 줄에 적용하는 규칙 기반 패턴을 사용한다. 따라서 줄을 특이하게 나누거나 따옴표가 든 정규식 리터럴을 쓰거나 매크로를 사용하는 코드에서는 놓치거나 잘못 탐지할 수 있다. 테스트 하나를 지운 뒤 무관한 간단한 테스트 하나를 추가하면 전체 변경에서 테스트 수가 줄지 않아 탐지되지 않을 수도 있다.

소스 분석은 JavaScript·TypeScript, Python, Go, Rust, Java와 Kotlin 테스트 주석에 한정된다. Ruby, C#, C/C++, PHP, Swift, 사용자 지정 테스트 언어는 분석하지 않는다. 다만 지원하지 않는 언어를 쓰더라도 CI 설정 변경은 검사 대상이 될 수 있다. fakegreen-ignore-file은 변경분에 추가된 줄이나 주변 문맥에 있을 때만 적용된다.

도구가 에이전트의 설정 변경으로 꺼질 가능성도 있다. 훅 설정 변경은 변경 내역에 드러나므로, 사전 커밋 훅이나 GitHub Actions를 함께 사용하고 .claude/, .codex/, .gemini/, .fakegreenrc.json의 변경을 CI 설정 변경처럼 검토하라는 것이 Almeida의 안내다. Cursor와 Aider는 차단형 작업 종료 훅을 지원하지 않는 점도 설정 전에 고려해야 한다.

설치와 사용 조건은 저장소의 fakegreen 안내에 있다. MIT 라이선스이며 명령줄 도구와 훅, 사전 커밋 훅, GitHub Action은 무료로 제공한다고 Almeida는 밝혔다. 단독 유지보수자가 개발하고 있으며, 여러 저장소를 위한 호스팅 버전은 아직 만들지 않았다. 원문은 오탐률의 독립 검증 결과나 지원 언어를 늘릴 시점은 밝히지 않는다.

부록: 원문 예시

text
](https://github.com/fitzyracing1/fakegreen/blob/main/docs/demo.cast), [
text
](https://github.com/fitzyracing1/fakegreen/blob/main/docs/demo.gif), [
text
. If your agent commits as it goes, scan the whole branch instead: