catfish-1234/proctor
Catches AI coding agents gaming their test suites: deleting tests, skipping them, weakening assertions, or hardcoding outputs to fake a green build.
Changelog
All notable changes to this project are documented here.
The format follows Keep a Changelog, and this project adheres to Semantic Versioning.
1.0.0
First release. Nothing before this reached npm, so this entry describes the whole tool rather than a delta from a version nobody can install.
What ships
- 27 checks in two families. RH001 to RH014 read the test suite and the code directly under
it, and are enabled by default. WI101 to WI113 read shipped code for the ways an agent fakes
completion without touching a test at all; they ship as beta and opt-in, behind
proctor check --wi(or--all-checks), or by naming the IDs inenabledinproctor.config.json. - The honest-completion ruleset, deployed to 30 agents from one canonical source, with
drift-checkto prove every copy still matches it. proctor check, plussetup,agents,uninstall,watch,score,statusline,approve,badge,drift-checkandbench. Output as text, JSON, SARIF 2.1.0 or Markdown.- A git pre-commit hook and a Claude Code Stop hook, and a GitHub Action that uploads SARIF and writes a job summary.
- 25+ languages, with the per-language, per-check support matrix and every known gap
documented in
docs/LANGUAGES.md. - A reproducible benchmark and an adversarial red-team corpus, both runnable from a clone.
Worth knowing before you install
- Installing writes nothing to your repository.
npm installprints exactly whatproctor setupwould write, and where, and stops there.PROCTOR_AUTO_SETUP=1opts into the install-and-wire behaviour;PROCTOR_NO_POSTINSTALL=1silences the notice. - Enforcement config is read from the committed
proctor.config.json, never the working tree, so a change cannot disable proctor in the same breath it cheats in. - An inline
proctor-ignoremarker only suppresses when it predates the diff. A marker added alongside the change it excuses is a self-approval and does not count. approvedTestChangesdowngrades a finding toinfo, it never hides one. An approved finding still prints, still reaches--jsonand SARIF, and still withholds the honest-pass badge.- The pre-commit hook fails closed and the Stop hook fails open, deliberately: a guard that breaks should not become a wall at the end of an agent turn, but it must not mint a pass from missing evidence at commit time.
- The deterministic core makes no network call.
--aiis an opt-in extra and needs an API key you set yourself.
Measured
- 139 of 139 planted cheats caught, 0 of 33 near-miss fixtures flagged. Both counts are
recomputed from
fixtures/by the test suite rather than written by hand. - 76 of 76 adversarial diffs caught with 0 of 24 controls flagged, across eleven red-team rounds.
- 3.9% of 689 real human commits from 20 maintained repositories produce any finding from the
default check set, and 2.47% produce a blocking one, most of which are the check correctly
noticing that somebody deleted a test. See
sandbox/REALWORLD_FP_REPORT.md. - Two complete live benchmark runs show no difference between the proctor-on and proctor-off arms. The README says so plainly rather than quoting the first run's one-task gap: this release claims a detection rate and explicitly refuses a prevention rate.
Pre-release development history
Everything below happened before the first release, so none of it is a change to a published version. It is kept because the reasoning is worth reading, not because any of it shipped separately.
The final pass
Changed
- The WI1xx work-integrity family is opt-in. It ships as beta: run it with
proctor check --wior--all-checks, or list the IDs inenabledinproctor.config.jsonto apply it everywhere, hooks included. No check was removed or weakened; thirteen checks reading arbitrary source across 25+ languages is a larger false-positive surface than the RH family's and has had less real-world exposure, so it earns default-on in a later release rather than assuming it. - Installing no longer writes to your repository.
npm installnow prints exactly whatproctor setupwould write, and where, and stops.proctor setupis unchanged and still does the whole install;PROCTOR_AUTO_SETUP=1restores the previous install-and-wire behaviour, andPROCTOR_NO_POSTINSTALL=1silences the notice. An npm package that silently rewrites git hooks spends exactly the trust this tool is selling. - RH011 counts suppressions per file rather than per change, and a suppression whose author wrote down why it is there no longer counts toward the threshold. Its message now reads "N of them in this file".
- The README CI example uses the
catfish-1234/proctor@v1moving tag, with the full-SHA pin documented beside it as the stricter alternative. - Releases publish with
npm publish --access public --provenance.
Fixed
Every fix in this group came out of a sweep of 689 human commits from 20 maintained repositories
(sandbox/REALWORLD_FP_REPORT.md). Together they take the share of real commits producing any
finding from 10.3% to 7.7%, and the share the default check set flags from 5.8% to 3.9%, with the
Part A catch rate unchanged at 12/12.
-
checkno longer analysesnode_moduleson a first run. Untracked discovery relies on.gitignore, which a brand-new repository does not have yet, sonpm init -y && npm i @kavishdua/proctor && npx proctor checkreported 73 errors inside other people's packages. Dependency and build trees are now skipped whether or not they are ignored. Tracked files are untouched. -
WI103 no longer reads a
finallyblock as unreachable code.} finally {closes one block and opens a sibling whose body sits at the same indentation, so the first statement of everyfinallyafter an earlyreturnwas reported as bypassed. This was the single largest false-positive source in the sweep. -
RH004 and RH005 no longer read a relocated block as a gutted one. A line deleted and re-added in the same chunk was moved, not written, so it now takes part in neither side of the return-pairing. The same locality bug as the earlier chunk-wide pairing fixes, on the move axis.
-
WI111 no longer says a test file "asserts nothing" when it plainly does. It counted only the declarations the diff removed, so removing two cases from a forty-test file produced that message. A surviving declaration on an unchanged context line now suppresses it. A deleted build config or a deleted
*.test-d.tstype-test file is no longer reported as a deleted implementation. -
RH012 no longer reads a YAML comment or a composite action's input key as a test command. The removal branch requires the line to be somewhere a command actually runs.
-
The WI family no longer treats markup and data files as code. A build script embedded in a TOML string had its
catch {}reported as a swallowed error in shipped code. -
RH001 reports the whole test title when it contains the other quote character. The truncated title was also the key deleted tests are paired against, so distinct tests collapsed onto one.
-
check --uncommittedand the Claude Stop hook now include untracked, non-ignored files. A new test or config file previously sat outsidegit diff HEADand was invisible until staged. -
Verifier exceptions no longer become an empty finding set and an honest-pass receipt. Verifier and unexpected command failures exit
3, rather than Node's warning-only code1; pre-commit checks therefore fail closed while the Stop hook retains its documented infrastructure policy. -
Benchmark runs now reject timeouts, failed agents, and no-change runs; preserve paired on/off denominators; and refuse to overwrite an evidence CSV with partial results.
-
npm run verify:packnow locates Git Bash on Windows instead of assumingbashis onPATH. -
Production GitHub workflows and the composite action now pin every third-party action to a full commit SHA while retaining the reviewed release tag in a comment.
-
Packed-package verification now requires a real clean verdict instead of accepting any fast failure; the benchmark runner also settles reliably on timeout and caps output exactly.
-
Semgrep now blocks on production findings while scanning deliberately vulnerable fixtures in a separate advisory step.
-
Bare
proctor checknow covers staged, unstaged, and untracked changes by default instead of silently ignoring staged work;--stagedand--baseretain their narrower explicit scopes.
Added
- The WI1xx work-integrity family, WI101 through WI112, reading shipped code for the ways an
agent fakes a finished job without touching a test at all: an error swallowed, a guard deleted, a
"not implemented" marker shipped, real I/O replaced with canned data, a type widened to
any, a security control switched off, the code under test deleted. Opt-in and marked beta for this release, see Changed above. - RH014 detects reduced property/fuzz counts, collection slicing/filtering, contracted loop bounds, and rows removed from parameterized tests while their names and assertions survive.
- WI113 detects benchmark workload reductions, dependency/version-floor rollbacks, and unexplained fixed delays used instead of fixing the exposed behavior.
- A real-world false-positive corpus.
sandbox/realworld/clones 20 maintained open-source repositories and replays proctor over recent human commits, which is how every fix in this release's Fixed section was found.sandbox/REALWORLD_FP_REPORT.mdhas the numbers and the triage; the harness issweep.mjs,compare.mjsandlines.mjsbeside it. check --wiandcheck --all-checks, for running the beta WI family without editing config.- A feature-request issue template, and a
languagefield on the false-positive template. assets/social-preview.png, the 1280x640 card GitHub shows when a repository link is shared, generated byscripts/social-preview.ps1.- Red-team coverage now spans 76 adversarial diffs and 24 neighboring legitimate controls. All 76
are caught and all controls remain silent, including expression-based exit laundering, focused
test commands, CI matrix/trigger contraction, diagnostic suppression, expected-failure modifiers,
test-environment branches, and an actual Git
assume-unchangedstate that produces no diff.
Changed
- Existing rules now cover exception-type broadening, constant conditional/expected-failure test modifiers, coverage collection disabled, source paths excluded from CI triggers, snapshot update flags in the normal test command, shell pipeline/background status loss, and removed/downgraded production error reporting.
proctor checkrefuses tracked files hidden withassume-unchangedorskip-worktreeoutside a legitimate sparse checkout, closing a repository-state bypass no diff verifier can observe.
Earlier work
Fixed
-
The Stop hook now sees unstaged changes. It ran
check --staged, but an agent finishing a turn has edited the working tree and staged nothing, so the guard saw an empty diff and allowed every unstaged test deletion through. It now runscheck --uncommitted. -
setupno longer writes all 30 adapter files into every repository. It detects which agents a repository actually uses, from their own config files and directories, and installs only to those. A repo with no agent config getsAGENTS.md, the cross-vendor standard.--allrestores the previous behaviour. -
setupexits nonzero when an adapter path could not be written. A partial install that reported success was indistinguishable from one that worked. -
The Claude Code Stop hook is only installed when the repo uses Claude Code, rather than dropping
.claude/settings.jsoninto repositories that don't. -
proctor scoregoes through the shared diff helper, so the per-line length cap that bounds worst-case regex time applies when scoring history. It previously re-implementedgit diffinline and skipped it. -
proctor benchexplains an empty task pool instead of reporting it as an invalid--tasksvalue. The corpus ships with the repository, not the npm package. -
Documentation corrections: RH013 was missing from the configuration, language, and methodology docs; several cross-document links pointed at the wrong file;
docs/CLI.mdquoted stale--explainoutput. -
A shared file whose own prose contained the literal
<!-- proctor:start -->marker could lose content, or be deleted entirely, on uninstall. The block pattern matched from the user's stray marker to proctor's real end marker, so removing "the block" took everything in between with it. A conventions file that documents how proctor works is an ordinary thing to write, and it is exactly the file proctor merges into. The pattern is now tempered so a block body cannot span another start marker. -
uninstallaborted on the first unremovable path, printing nothing at all, so a failure partway through the roster left files already deleted with no record of which. Each item is now attempted independently, failures are reported by name, and a partial uninstall exits nonzero. -
setupcrashed with a rawTypeErroron a settings file that was valid JSON but an odd shape (hooks.Stopan object rather than an array,hooksa string, anullor array root), leaving a half-finished install. Those are now reported through the existing "fix it and re-run" path, and a root shape thatJSON.stringifywould silently drop is no longer reported as a successful install. -
uninstallleft.husky/pre-commitin place on a Windows clone while reporting that proctor was not installed. The hook is a committed file, socore.autocrlfchecks it out with CRLF and the byte-exact comparison failed. Line endings are normalized now. -
RH004 fired on ordinary guard clauses. Its special-case pattern matched negated comparisons, because the
!was absorbed by the character class before the operator, soif (result.status !== 0) return falseread as hardcoding an answer for input0. A negated comparison is the opposite shape: it refuses everything that is not the literal, which is a validation guard, and no cheat can be written that way.if (dir === '') return falseis now excluded too, on the same grounds as the existing.length === 0exclusion. Both were found by running proctor against its own diff. Error-severity false positives on shapes this common are the worst kind, since they block a commit for honest code. -
The Stop hook no longer blocks a turn during a merge, rebase, cherry-pick, or revert, where the working tree carries the incoming branch's changes and a test that branch deleted would read as this turn deleting it. The pre-commit hook still guards the resolution.
Added
proctor uninstallremoves everythingsetupinstalled, and nothing else. A shared file keeps your own content and loses only the managed block; a foreign pre-commit hook and any otherStophook entry are left alone.--dry-runshows what would go.proctor agentslists every supported agent and whether this repository appears to use it.proctor badgeprints the honest-pass badge as Markdown you can paste into a README or PR. The README told people to do this; no command produced it.check --uncommittedanalyses staged and unstaged changes together.setup --allandsetup --agents <ids>, and the same flags oninstall-skill.- Agent Plugins 1.0.0 manifest at the repository root, plus
skills/and.agents/skills/copies of the ruleset. One artifact covering ChatGPT, Codex, Cursor, GitHub Copilot, Kiro and VS Code. brandingonaction.yml, without which GitHub Marketplace rejects the listing.- A root Cursor marketplace manifest, without which Cursor cannot see a plugin that lives in a subdirectory.
CONTRIBUTING.md,SECURITY.md,docs/TROUBLESHOOTING.md, this changelog, and issue templates.