Claude Code said "Done." My tests said otherwise. Here's the 20-line fix. A developer published a 20-line Python Stop hook for Claude Code that blocks the agent from ending its turn until a configured verification command (such as `npm test && npx tsc --noEmit`) passes, feeding the failure output back to Claude via stderr and exit code 2. The hook reads its check from `.claude/verify.txt`, caps consecutive blocks at four to avoid loops, and is registered in `.claude/settings.json`; the author cites Anthropic's best-practices guidance and Claude Code creator Boris Cherny's claim that such feedback loops improve result quality 2–3×. If you use Claude Code, you've seen this: claude: Done Partial refunds are implemented. ✅ Then you run the tests yourself and four of them fail. Claude isn't lying. It stops when the work looks done, because it has no other signal. If nothing tells it the tests are red, "looks done" is all it has. So you become the test runner: you check, paste the errors back, wait, and check again. This post shows how to make Claude run that check itself, every time, before it's allowed to say it's finished. Anthropic's best practices for Claude Code https://code.claude.com/docs/en/best-practices put this first: give Claude a way to verify its work, such as tests, a build, a linter or a script, anything that returns pass or fail. Boris Cherny, who created Claude Code, has said https://x.com/bcherny/status/2007179832300581177 that this feedback loop improves the quality of the result 2–3×. There are three places you can put that check: | Where | How reliable | |---|---| | In your prompt "run the tests after" | Works when you remember to type it | | In CLAUDE.md | Advice. Claude usually follows it, sometimes not | | In a hook | Runs every time. Claude can't skip it | Hooks are scripts Claude Code runs at fixed points in its loop. The one we want is the Stop hook, which runs whenever Claude tries to end its turn. If a Stop hook exits with code 2 , Claude Code doesn't let the turn end. It sends whatever the hook printed to stderr back to Claude, and Claude keeps working. That's the whole trick. Save this as .claude/hooks/verify-gate.py in your project: python /usr/bin/env python3 import json, os, subprocess, sys data = json.load sys.stdin project = os.environ.get "CLAUDE PROJECT DIR", os.getcwd config = os.path.join project, ".claude", "verify.txt" if not os.path.exists config : sys.exit 0 no check configured: let Claude stop if data.get "consecutive blocks", 0 = 4: sys.exit 0 blocked 4 times in a row: let Claude stop instead of looping command = open config .read .strip result = subprocess.run command, shell=True, cwd=project, capture output=True, text=True, timeout=300 if result.returncode = 0: tail = result.stdout + result.stderr .strip .splitlines -40: print "Verification failed:\n" + "\n".join tail + "\nFix the failures above, then finish. Do not weaken or skip tests.", file=sys.stderr sys.exit 2 exit 2 = block the stop; stderr is sent back to Claude Register it in .claude/settings.json : { "hooks": { "Stop": { "hooks": { "type": "command", "command": "python3 \"${CLAUDE PROJECT DIR}/.claude/hooks/verify-gate.py\"", "timeout": 330 } } } } On Windows, python instead of python3 . Then put your check in .claude/verify.txt : npm test && npx tsc --noEmit Restart Claude Code. The next time it says "Done" with red tests, this is what happens: claude: Done. Partial refunds are implemented. Stop hook: Verification failed: ✗ refund.test.ts › partial refund rounds to cents Expected 12.35, received 12.349999 Fix the failures above, then finish. Do not weaken or skip tests. claude: The rounding is off. Fixing src/orders/refund.ts:42… claude: All 48 tests pass and types are clean. Done. You didn't paste anything. Claude found out it wasn't done, and fixed it. The hook is only as good as the command in verify.txt . A good one: vitest needs vitest run , and jest needs --watchAll=false in some setups. pytest tests/billing -q . Examples: npm test && npx tsc --noEmit pytest -q && ruff check . go test ./... && go vet ./... cargo test && cargo clippy -- -D warnings Windows gotcha: the command runs through cmd.exe , which doesn't treat single quotes as quotes. python -c 'exit 1 ' silently passes there, because Python evaluates a string literal and exits 0. Use double quotes inside verify.txt . I found this out when my own README example "passed" a test that should have failed. It works, but a real session exposes some edge cases: git status is clean, or when the tree hasn't changed since the last passing run. timeout=300 raises an exception but can leave child processes running a dev server, a test watcher . Better: kill the whole process tree, and report the timeout as a failure with advice. consecutive blocks may not always be there. I handled all four in a stdlib-only version that works on Windows, macOS and Linux. It's free and MIT licensed: verify-gate-hook on GitHub https://github.com/elijahmanlockedin112/verify-gate-hook . Setup is the same three steps. Write the check before the code. If you write verify.txt first and run it once, it should fail , because the feature doesn't exist yet. If it passes, it isn't testing the feature. Tighten it before you start. Watch for weakened tests. The hook's message tells Claude not to weaken or skip tests, and it usually listens. Still skim the diff for edited assertions. A gate is only as honest as the test it runs. The gate answers "is it done?" The harder part is deciding what "done" means before Claude starts. I ended up building a full Claude Code setup around this loop: /lock-in has Claude ask you the hard questions, then write a spec with a single verification command. It writes that command to verify.txt , checks that it fails first, builds step by step against it, and finally has a fresh subagent review the diff against the spec. rm -rf ~ , force-pushes to main , and reading .env before those commands ever run. /handoff → /clear → /pickup saves your progress to a file and resumes from it in a fresh session, so your context stays small and your usage limit lasts longer. That's the Locked In Kit https://lockedin.clarionproductlabs.com/ : 18 skills, 6 subagents, 5 hooks, 8 CLAUDE.md templates and a playbook. The first 20 people get 30% off with code LAUNCH . The hook above is free and works fine on its own, though. Add it to one project today and see how often Claude "finishes" with failing tests. Independent project, not affiliated with Anthropic. Hook behavior is from the Claude Code hooks reference https://code.claude.com/docs/en/hooks . If something behaves differently in your version, trust the docs.