Prove It Works: make your AI agent run the checks before it says done
Prove It Works runs your project's real tests, build, typecheck and lint, shows the real output, and inspects the change for quiet fakes like skipped tests or a test script that cannot fail. The first line is one of four verdicts, so "done" means proven, not hoped.
Always free. Works with Claude Code, Claude.ai, Cursor and Codex.
When to use it
Your agent says "Done, all tests pass." Sometimes that is true. Sometimes the tests never ran, a failing test got a .skip, or the test script now ends in || true. Prove It Works makes the agent show its evidence before it says "done".
- "Did you actually run the tests?"
- "Is it really done, or does it just look done?"
- "Prove it works before I commit this."
- "Run the checks before you open the PR."
- "Don't do that again." (after you correct a mistake: the agent turns it into a rule that every later proof checks)
You usually type nothing. The skill tells the agent to run it at the end of every coding task. You can also ask in plain words: prove it works, did you run the tests? or run the checks.
How it works
- The agent saves your task and runs the script. It writes the request (plus any acceptance criteria) to a temporary file outside the repo and runs
scripts/prove.py <repo> --task-file <file>. The script works out the change by itself: on a feature branch, everything since the default branch (commits and uncommitted work); on the default branch, your uncommitted work, or the last commit if it is from the last 24 hours. - It runs your real checks. It finds the project's tests, build, typecheck and lint commands and runs them with a timeout (300 seconds each, 540 in total by default) in an offline environment. Anything named or doing deploy, release, publish, migrate, install or network work is never run automatically. A check that could not run is NOT RUN, never PASS and never FAIL. A test run whose output reports failures counts as a failure even if the command exits 0.
- It inspects the change for quiet fakes. Skipped or weakened tests, test commands that cannot fail, swallowed errors, hardcoded results, files outside the task, task items no code implements, debug leftovers, secrets, unasked dependencies, edited migrations and broken project rules. Each problem comes with the file and line.
- The first line is the verdict. One of four, and the agent must quote it word for word as the first line of its reply, followed by the checks table (command, result, exit code, time).
- The agent does its own review pass. Using the skill's review checklist, it writes your task as a numbered list of requirements and puts the
file:linethat delivers each one next to it, or MISSING. It reads every changed test, decides on each "Check these" item (real problem, intended, or not applicable) and checks the project rules that can only be checked by reading. Any MISSING row means not done, whatever the first line says. - Fixes are the agent's job, not the script's. The scripts never edit your code. If the verdict is NOT DONE, the agent fixes the code (never the check: no skipping, loosening or muting) and proves again. It does not install anything without asking you first.
| Verdict | What it means | What the agent says |
|---|---|---|
PROVEN: ... | A task was given, every check ran and passed, no problems in the change. | Done, with the proof and the requirements list. |
CHECKS PASS (...) | Nothing failed, but something was not covered: no task given, no test command, nothing to inspect, or a check that fails only on lines the change did not touch. | The checks pass, and exactly what was not covered. |
NOT VERIFIED: ... | Checks could not run: dependencies or tools missing, time budget used up, or unsafe to run. | It is not proven. Names the command that would let the checks run and asks once. |
NOT DONE: ... | A check fails, or the change has a problem. | "Not done yet", then each item. |
Optional: run it every time Claude Code stops
Claude Code can run a command each time the agent finishes a turn (a Stop hook). This snippet from the skill's README keeps the agent working while the proof says NOT DONE. It is not installed for you. Add it yourself to .claude/settings.json in your project, or to ~/.claude/settings.json for every project, and adjust the path to where you unzipped the skill:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/skills/prove-it-works/scripts/prove.py --hook --only tests,lint,typecheck,rules --timeout 240",
"timeout": 600
}
]
}
]
}
}
- When the proof says NOT DONE, the hook blocks the stop with the failing checks and problems, so Claude keeps working on them. It blocks again only if the code changed since the last block, and gives up after 8 attempts on the same session.
- It never blocks when checks could not run (dependencies not installed, no test command, a timeout). It tells you once instead.
- It reads the task from the session transcript, does nothing outside git, and does not rerun anything when nothing changed since the last proof.
What it checks
Your real commands
- package.json scripts, run with npm, pnpm, yarn or bun (whichever
packageManageror your lockfile says). - Makefile targets, pyproject task tables, pytest or unittest, ruff, flake8, mypy.
- cargo, go, Maven, Gradle, Bundler, Composer, .NET and Deno, plus documented and conventional commands.
- Missing
node_modulesor a missing tool means NOT RUN. A global eslint or tsc is never used in place of the project's own.
Tests that stopped testing
- Focused or skipped tests added by the change (
.only,.skip,xit,@unittest.skip,pytest.mark.skip,{ skip: true }). - Deleted or loosened assertions, and tests that cannot fail.
- New test files your test command never picks up.
- Snapshots and golden files re-recorded to match a bug.
- Lowered coverage thresholds.
Checks that cannot fail
- Test commands and CI jobs changed so they cannot fail (
|| true,continue-on-error, a test step removed). - Swallowed errors, special-cased test inputs and hardcoded results.
@ts-ignore,eslint-disableand# type: ignoreadded to get to green.
The change versus the task
- Files changed outside the task.
- Task items such as a named function or a
--flagthat no code implements. - Changed public function signatures.
Leftovers and risky edits
- Debug leftovers, TODOs, stubs and conflict markers.
- Secrets and
.envfiles. Secrets in command output are masked before they are shown. - Dependencies added without asking, and lockfiles not updated.
- Edited migrations, and schema changes without a migration.
- Generated or large files, and screenshots or other binaries nothing uses.
Your project rules
- Rules from CLAUDE.md, AGENTS.md, .cursorrules, .cursor/rules, .github/copilot-instructions.md, CONTRIBUTING.md and .windsurfrules.
- Rules a script can check (package manager, forbidden imports, files not to edit, required commands, file naming) become checks. The rest are listed for the agent to check by reading.
- Your own "never twice" rules in
.techfix/rules.md: a pattern that must not appear, a path that must not change, or a command that must pass.
What it can't see
- CI-only differences: another runtime version, secrets or services that only exist in CI.
- Flaky or order-dependent tests. One green run is not proof they are stable.
- Anything that needs a human or a browser: visual layout, real user flows, copy.
- Whether the change does what the task meant beyond the names, files and flags the task mentions. A flag that is parsed but ignored, or a wrong number, is caught by the agent's requirements list, not by the script.
Worked examples
Three small git repos, each with an uncommitted "agent change" and a TASK.md. The outputs below are real runs of scripts/prove.py --task-file TASK.md, trimmed only by cutting lines.
1. A failing test got a .skip, and the agent said "all tests pass"
A Node cart module tested with node --test. The task: orders over $100 get a 10% discount; do not change the tests. The agent's fix did not work, so it skipped the acceptance test and reported success.
// src/cart.js
if (amount > DISCOUNT_THRESHOLD) {
console.log("discount applied", amount);
return amount * (1 - DISCOUNT_RATE / 10);
}
// test/cart.test.js
test.skip("applies a 10% discount over $100", () => {
assert.equal(total(big), 108);
});
NOT DONE: 0 failing checks, 2 problems in the change
Change: uncommitted changes vs HEAD (2 files). Task: skipped-test/TASK.md. Took 0.5s (checks 0.4s).
## Checks (1)
| # | Check | Command | Result | Exit | Time |
|---|---|---|---|---|---|
| 1 | tests | `npm run test` | PASS (3 tests, 1 skipped) | 0 | 0.3s |
**Output: tests `npm run test`** (last 4 lines)
```
# cancelled 0
# skipped 1
# todo 0
# duration_ms 125.696549
```
## Problems in the change (2)
### 1. Tests weakened in test/cart.test.js
- **Finding:** In test/cart.test.js the change leaves the tests weaker than before: a test is skipped; 1 test case removed.
- **Evidence:**
- test/cart.test.js:17: test.skip("applies a 10% discount over $100", () => {
- removed test/cart.test.js:17: test("applies a 10% discount over $100", () => {
- **Impact:** The suite now passes without checking what it used to check, so a green run no longer proves the behaviour works.
- **Fix:** Restore the removed or loosened assertions and un-skip the tests, then make the code pass them. If an expectation really changed, assert the new exact value instead of a looser one.
- Causes: 6 (tests weakened), 5 (tests did not really run).
### 2. Debug leftovers in src/cart.js
- **Finding:** The change leaves a debug leftover in src/cart.js (console.log).
- **Evidence:** src/cart.js:12: console.log("discount applied", amount);
- **Impact:** Debug output leaks data into logs and the browser console; a stray debugger or breakpoint() halts the program.
- **Fix:** Delete the debug lines (or use the project's logger if the output is really needed).
- Cause: 9 (debug leftover).
…
Before you say done: you can't. Say "not done yet" with the first line above, fix each failing check and problem, then run the proof again. Never call a NOT RUN check passed.
The test command exited 0, so a plain "tests pass" would look true. The skill tells the agent to lead with the verdict line, then confirm each finding by reading the line: the test.skip at test/cart.test.js:17 was added by this change (not a platform guard), and the console.log at src/cart.js:12 is debug output. It fixes the code, not the check. Un-skipping the test shows the real failure (expected 108, actual 118.8). The review checklist's "silent substitutions" step points at the cause: the task says 10%, and the code divides the rate by 10, so it applies 1%. After the fix below, the proof on the fixed copy reads PROVEN: all 1 check passed, no problems in the change.
- console.log("discount applied", amount); - return amount * (1 - DISCOUNT_RATE / 10); + return amount * (1 - DISCOUNT_RATE); -test.skip("applies a 10% discount over $100", () => { +test("applies a 10% discount over $100", () => {
2. The work really is done: PROVEN, with two items to look at
A small Python CLI tested with unittest. The task: add --limit N to notes search, reject 0 or negative values the same way as --page, and add tests. The project's CLAUDE.md says standard library only, no new dependencies.
s.add_argument("--page", type=positive_int, default=1)
s.add_argument("--limit", type=positive_int, default=None,
help="print at most N results")
...
shown = hits[start:start + PAGE_SIZE]
if args.limit is not None:
shown = shown[:args.limit]
PROVEN: all 1 check passed, no problems in the change (2 items to review) Change: uncommitted changes vs HEAD (8 files). Task: limit-flag/TASK.md. Took 0.3s (checks 0.1s). ## Checks (1) | # | Check | Command | Result | Exit | Time | |---|---|---|---|---|---| | 1 | tests | `python -m unittest` | PASS (9 tests) | 0 | 0.1s | **Output: tests `python -m unittest`** (last 4 lines) ``` ---------------------------------------------------------------------- Ran 9 tests in 0.008s OK ``` ## Check these (Claude: open each one and decide) (2) - **Generated files in the change: notes/__pycache__/ (3 files).** The change adds notes/__pycache__/__init__.cpython-313.pyc: generated or installed output (__pycache__/); untracked, probably left by a local test run, but not gitignored. Evidence: notes/__pycache__/__init__.cpython-313.pyc (new file, 169 bytes); notes/__pycache__/cli.cpython-313.pyc (new file, 2 KB). Cause 17. - **Generated files in the change: tests/__pycache__/ (3 files).** The change adds tests/__pycache__/__init__.cpython-313.pyc: generated or installed output (__pycache__/); untracked, probably left by a local test run, but not gitignored. Evidence: tests/__pycache__/__init__.cpython-313.pyc (new file, 169 bytes); tests/__pycache__/test_cli.cpython-313.pyc (new file, 4 KB). Cause 17. ## Project rules (CLAUDE.md) Checked automatically (1): CLAUDE.md:5 do not edit notes.json. Rules Claude must check by reading (3): - CLAUDE.md:3: Standard library only. - CLAUDE.md:3: Do not add dependencies. - CLAUDE.md:4: Run the tests with `python -m unittest`. ## Notes - Task items matched mechanically: 2 found in the code (`--limit`, `--page`). The rest of the task (behaviour, edge cases, numbers) is for your review pass: review-checklist section 1. … Before you say done: quote the first line and the checks table, then do the review pass (references/review-checklist.md) for what the scripts can't judge.
The test run left __pycache__ folders behind. The script reports them under "Check these", not as problems, so the verdict stays PROVEN. The skill tells the agent to decide on each one: a cache left by a local run is not a defect; do not commit it, and suggest the .gitignore line. Then the requirements list from the checklist, each row mapped to code:
--limit Nis parsed: notes/cli.py:28.- It caps the printed results: notes/cli.py:41-42.
- Default is no extra limit: notes/cli.py:28 (
default=None). - 0 or negative rejected like
--page: notes/cli.py:28 uses the samepositive_int, so the same message and exit code 2. - Tests: tests/test_cli.py:36, 41 and 45, with exact
assertEqualvalues. The test count grew from 6 to 9.
The rules to check by reading hold: only argparse is used and no dependency was added.
+# .gitignore +__pycache__/
3. Dependencies are not installed: NOT VERIFIED, not a false FAIL
A Node invoice module that uses date-fns, cloned on a machine where npm install never ran. The task: add dueDate(invoice), the issue date plus netDays, defaulting to 30. The test command exits 1 because the package is missing.
import { addDays, format } from "date-fns";
...
export function dueDate(invoice) {
return addDays(invoice.issued, invoice.netDays ?? 30);
}
NOT VERIFIED: 1 check could not run (dependencies or tools are not installed); no problems found in the change Change: uncommitted changes vs HEAD (2 files). Task: missing-deps/TASK.md. Took 0.6s (checks 0.5s). **Not covered:** 1 check not run: `npm run test` (the package `date-fns` is not installed (no node_modules folder). Install the dependencies). ## Checks (1) | # | Check | Command | Result | Exit | Time | |---|---|---|---|---|---| | 1 | tests | `npm run test` | NOT RUN: the package `date-fns` is not installed (no node_modules folder). Install the dependencies, then prove again | 1 | 0.4s | ## Notes - Task items matched mechanically: 2 found in the code (`dueDate`, `netDays`). The rest of the task (behaviour, edge cases, numbers) is for your review pass: review-checklist section 1. … Before you say done: you can't claim it works. Quote the first line, tell the person which checks could not run and the exact command that would let them run (install, tool), and ask once. Never call a NOT RUN check passed.
The test command failed, but not because of the code: the script read the error, saw that date-fns is not installed, and marked the check NOT RUN. So the agent cannot say "done", and it also does not report a failure that isn't there. The skill tells it to quote the verdict, name the command that would let the check run, and ask once instead of installing on its own: "Tests are NOT RUN: date-fns isn't installed (no node_modules). Want me to run npm install?" Its review pass still maps the task: dueDate at src/invoice.js:11, the 30-day default at src/invoice.js:12, and a test for each case. Those rows only count as proven once the tests actually run.
npm install
python3 <skill-dir>/scripts/prove.py . --task-file <task-file>
Results
On 7 blind "agent says done" cases (Oct 14), Claude with the skill got all 7 verdicts right and found every planted problem; so did plain Claude when explicitly told to verify. Its value is that it runs every time, automatically, so you do not have to remember to ask.
Install Prove It Works
Download prove-it-works.zip Free during launch. Checksums
python3 already; on Windows install it from python.org. Git is needed for the history checks.Claude Code
- Download
prove-it-works.zip(button above). - Unzip it into your personal skills folder so it works in every project:
Or into one project only (commit it to share it with your team):unzip ~/Downloads/prove-it-works.zip -d ~/.claude/skills/unzip ~/Downloads/prove-it-works.zip -d .claude/skills/ - Check it landed:
ls ~/.claude/skills/prove-it-works/SKILL.md - Use it. Claude Code picks up new skills without a restart (if
~/.claude/skillsdid not exist when your session started, run/reload-skills). Ask in plain words, for example “prove it works” before your agent says it is done, or type/prove-it-works.
Claude.ai (web and desktop app)
- Open Settings > Capabilities and make sure Code execution and file creation is on. Skills need it. (Available on Pro, Max, Team and Enterprise plans; on Enterprise an Owner enables Skills first.)
- In the Skills section, click Upload skill and choose
prove-it-works.zip. Do not unzip it. - Check that it appears in your Skills list and switch it on.
- Start a chat, attach your project as a zip (or paste the error or alert) and ask, for example “prove it works”. In Claude.ai the scripts run in Claude’s sandbox on the files you upload, not on your computer.
Prove It Works is built for coding agents that can run your tests (Claude Code, Cursor, Codex). In Claude.ai it can only check the files you upload.
Cursor
- Unzip into Cursor’s skills folder for all projects:
or into a project:unzip ~/Downloads/prove-it-works.zip -d ~/.cursor/skills/.cursor/skills/. Cursor also reads~/.claude/skills/, so a Claude Code install works in Cursor too. - In Agent chat, type
/and pick prove-it-works, or just ask: “prove it works”.
OpenAI Codex
- Unzip into your user skills folder:
or intounzip ~/Downloads/prove-it-works.zip -d ~/.agents/skills/.agents/skills/at the root of a repo. - Mention it with
$prove-it-works(or pick it from/skills), or just ask: “prove it works”.
Other agents, updates and removal
- Any agent that reads
SKILL.mdfiles: unzip into its skills folder. Each skill is a folder withSKILL.md,scripts/andreferences/. - Update: download the new zip and unzip it over the old one with
unzip -o. - Remove: delete
prove-it-works/from the skills folder. - Check the download:
shasum -a 256 prove-it-works.zipand compare with SHA256SUMS.txt.
FAQ
Does it change my code?
The scripts never edit your code. The only files they write are in .techfix/: rules.md when you add a "never twice" rule, and, with your OK, one line per run in ledger.md (verdict and counts, never code or secrets). Your project's own test and build commands run as they normally do, so they may leave their usual output, like the __pycache__ folders in example 2. Fixes come from the agent, as normal changes you can see.
Does my code leave my machine?
The scripts are Python standard library plus git and run locally. Checks run in an offline environment: nothing is installed or downloaded, and network, deploy and publish commands are never run automatically. The agent reads the report and your diff the same way it already reads your code during the session.
Which stacks does it work with?
Node (npm, pnpm, yarn, bun), Python (pytest, unittest, ruff, flake8, mypy), Rust, Go, Java (Maven, Gradle), Ruby (Bundler), PHP (Composer), .NET and Deno, plus Makefile targets and pyproject task tables. You need Python 3.8 or newer where your agent runs commands. The Stop hook is for Claude Code.
How long does it take?
The scripts take under a second on these examples (0.5 to 0.7 seconds each); Claude's review adds time. On a real project your own test suite sets the pace: each command gets 300 seconds by default and all of them together 540, after which the rest are marked NOT RUN.
How is this different from asking the agent "did you test it?"
When you explicitly tell it to verify, plain Claude can do this well: in our test it got the same results. The difference is that the skill runs every time, automatically, so you do not have to remember to ask. It also gives a fixed first line with one of four verdicts, and it never lets a check that did not run be reported as passed.
What does it cost?
Nothing. Prove It Works is free during launch.