Quick Answer
Telling an agent to "check your work" is a request it can forget. Write a definition of done into CLAUDE.md, AGENTS.md or .cursor/rules, then enforce it with a hook that runs your typecheck and tests when the agent tries to finish. Add a scanner for what tests cannot see: committed secrets, vulnerable or invented packages, and injection patterns. Rules only raise the odds that the agent checks; hooks run whether or not the model remembers.
Why "Please Check Your Work" Is Not Enough
Anthropic's documentation is direct about instruction files: Claude Code treats CLAUDE.md files "as context, not enforced configuration". A line that says "run the tests before you finish" is a request. In a long session with a full context window, an agent can skip it, run only the one test it just wrote, or report a typecheck it never ran.
The second problem is coverage. A typecheck and a linter catch broken code. Tests catch the behavior someone wrote down. None of them notices an API key pasted into a config file, a package name the model invented, or a route that checks that the user is logged in but not whether the record belongs to them. Each of those needs its own layer. Our guide to checking AI-generated code before shipping covers the manual pass; this one makes the agent run it.
The Four Layers at a Glance
| Layer | Where it lives | Enforced? | What it catches |
|---|---|---|---|
| Rules file | CLAUDE.md, AGENTS.md, .cursor/rules/*.mdc | No, the agent reads it as instructions | Nothing on its own; it defines "done" |
| Hooks | .claude/settings.json, .cursor/hooks.json | Yes, they run on every edit or stop | Type errors, lint errors, failing tests |
| Scanner as a tool | The agent's MCP configuration, or the hook itself | Called by the agent or by a hook | Secrets, vulnerable or non-existent packages, injection, unprotected routes |
| Tests that assert behavior | Your test suite | Run by the hooks | Regressions in the behavior you specified |
Step 1: Write the Definition of Done
Put it where the agent reads it at the start of every session. Claude Code reads CLAUDE.md. Cursor reads .cursor/rules and an AGENTS.md in the project root. Claude Code also reads AGENTS.md since version 2.1.277, but only when the repository has no CLAUDE.md. If you keep both, add the line @AGENTS.md to CLAUDE.md so each agent sees the same rules.
## Definition of done
Before you say a task is finished:
1. Run the typecheck and the linter. Fix every error.
2. Run the tests for the code you changed. Add a test if none covers it.
3. If you touched auth, API routes, database queries or payments,
run a security check on the changed files and fix critical and high findings.
4. Never add a dependency without confirming the package exists on the registry.
5. Report the commands you ran and their results. If anything failed, say so
and do not say "done".
In Cursor, the same text goes in a rule file such as .cursor/rules/definition-of-done.mdc. The alwaysApply field makes Cursor include it in every chat:
---
description: Definition of done for every coding task
alwaysApply: true
---
(the five rules above)
Step 2: Enforce It With Hooks in Claude Code
Hooks are commands Claude Code runs at fixed points in a session. Anthropic's hooks guide describes the point of them: certain actions always happen "rather than relying on the LLM to choose to run them". Two hooks cover most of the gap:
- PostToolUse on Edit and Write: fast feedback after each file change. If the command exits with code 2, Claude sees its error output.
- Stop: the gate. When Claude tries to end its turn, the hook runs. If it returns a block decision, Claude keeps working and reads your reason as its next instruction.
Add both to .claude/settings.json. This file can be committed so the whole team gets the same gate:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint-file.sh" }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/definition-of-done.sh" }
]
}
]
}
}
Both scripts below read the hook's JSON input with jq, so install it first (brew install jq on macOS, apt-get install jq on Debian and Ubuntu). Save them in .claude/hooks/ and make them executable with chmod +x .claude/hooks/*.sh (the Cursor script further down needs the same). If either step is missing, the hook fails and the agent carries on as if the checks passed.
The per-edit hook reads the edited file's path from the JSON Claude Code sends on standard input and lints only that file:
#!/bin/bash
# .claude/hooks/lint-file.sh
command -v jq >/dev/null || { echo "Install jq: this hook reads its input with it" >&2; exit 2; }
FILE=$(jq -r '.tool_input.file_path // empty')
case "$FILE" in
*.ts|*.tsx|*.js|*.jsx) ;;
*) exit 0 ;;
esac
if ! OUT=$(npx eslint "$FILE" 2>&1); then
echo "$OUT" | tail -n 30 >&2
exit 2
fi
The Stop hook runs the slower checks once, when Claude says it is finished:
#!/bin/bash
# .claude/hooks/definition-of-done.sh
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active // false')" = "true" ] || [ "$(echo "$INPUT" | jq -r '.loop_count // 0')" -gt 0 ]; then
exit 0
fi
if ! OUT=$( (npx tsc --noEmit && npm test --if-present --silent) 2>&1 ); then
jq -n --arg r "Typecheck or tests failed. Fix them before finishing: $(echo "$OUT" | tail -n 40)" \
'{decision: "block", reason: $r}'
fi
exit 0
The first if is the loop guard. Claude Code sets stop_hook_active when Claude is already continuing because of a Stop hook, and Cursor, which also runs these hooks, counts its follow-ups in loop_count. Either way the script allows one fix attempt and then lets the agent stop. Keep the guard: Claude Code ends a loop on its own only after eight blocks in a row with no tool call in between, and fixing a test always involves tool calls, so without it a test that cannot pass keeps the session running until you interrupt it.
Keep the Stop hook fast. Run the typecheck and the unit tests for the area you are working in, and leave the full suite to CI. --if-present skips the tests when the project has no test script yet. In a Vite or Lovable project the root tsconfig.json only points to other configs, so npx tsc --noEmit there checks nothing; use npx tsc --noEmit -p tsconfig.app.json instead. If you prefer not to write scripts, Claude Code also supports an experimental agent hook type, where a subagent runs the tests and decides whether Claude may stop.
Step 2 in Cursor: hooks.json and a Follow-Up Message
Cursor has its own hooks in .cursor/hooks.json. Project hooks run from the project root. The afterFileEdit hook receives the edited file's path, and the stop hook can return a followup_message. Cursor submits it as the next user message, so a failing check sends the agent back to work. Cursor caps these follow-ups at 5 per script by default (the loop_limit option).
Cursor also runs the hooks in .claude/settings.json unless you turn off its Include Third-Party Plugins, Skills, and Other Configs setting, which is on by default. If your repository already has the Claude Code hooks above, skip this file, or turn the setting off so the checks do not run twice.
{
"version": 1,
"hooks": {
"stop": [{ "command": ".cursor/hooks/definition-of-done.sh" }]
}
}
#!/bin/bash
# .cursor/hooks/definition-of-done.sh
STATUS=$(jq -r '.status')
[ "$STATUS" = "completed" ] || exit 0
if ! OUT=$( (npx tsc --noEmit && npm test --if-present --silent) 2>&1 ); then
jq -n --arg m "Typecheck or tests failed. Fix them, then finish: $(echo "$OUT" | tail -n 40)" \
'{followup_message: $m}'
fi
Step 3: Give the Agent a Scanner for What Tests Miss
Tests and a typecheck prove the code runs as specified, not that it is safe to ship. There are two ways to add the missing layer.
Deterministic tools in the same hooks. Gitleaks finds secrets, Trivy or OSV-Scanner finds vulnerable dependencies, and Semgrep or OpenGrep finds injection patterns. All are free and fast. You maintain the rules and read the output. Our comparison of Snyk alternatives covers what each one scans.
A scanner the agent calls as an MCP tool, so it checks files while it writes them. VibeDoctor (vibedoctor.io) works this way. Paste one line into Claude Code, Cursor or Codex:
connect to vibedoctor using https://vibedoctor.io/mcp/start
The agent registers the repository and installs the MCP server; you click one link to connect the repository on GitHub and restart the session. From then on the agent can call:
vibedoctor_checkon the files it changed. Optionally pass a category (security,imports,performanceorstructure) to get only that kind of finding.vibedoctor_statusfor project health and open findings.vibedoctor_graph(VibeDoctor X-Ray) for callers and blast radius before it edits shared code.
vibedoctor_setup writes the VibeDoctor part of the rules: when to call each tool, and to run a check before a task counts as done. It returns CLAUDE.md, .cursor/rules, .windsurfrules or .github/copilot-instructions.md for the agent to create, so keep your own definition of done next to it. If that lives in AGENTS.md, add @AGENTS.md to the new CLAUDE.md, because Claude Code stops reading AGENTS.md once a CLAUDE.md exists. Every new account starts with a Free Week: one project, one scan a day and 50 MCP calls a day, with no card required; status and setup calls never count.
Be clear about what it is not. It does not run your tests or replace your typecheck, so keep the hooks from Step 2. It does not decide which user may see which record either; read the diff for that, or run Claude Code's /security-review on the branch.
Step 4: Make the Tests Worth Running
A hook that runs tests is only as good as the tests. Agents tend to write tests that pass by construction: they call the function they just wrote and assert whatever it returns. Ask for tests from the requirement before the implementation, run them once to see them fail, and reject any test with an empty body or no assertion. We cover the patterns in AI tests that assert nothing.
What Each Layer Catches
| Problem | Rules file | Hooks (typecheck, lint, tests) | Scanner | Human review |
|---|---|---|---|---|
| Type and syntax errors | Asks | Catches | No | Sometimes |
| Broken behavior you specified | Asks | Catches if a test covers it | No | Sometimes |
| Committed secrets | Asks | No | Catches | Sometimes |
| Invented or vulnerable packages | Asks | No | Catches | Rarely |
| Unprotected API route | Asks | Only with a test for it | Catches the common patterns | Catches |
| Wrong user can read a record | Asks | Only with a test for it | Rarely | Catches |
A Ten-Minute Setup
- Add the definition of done to
CLAUDE.md, or toAGENTS.mdplus.cursor/rules. - Install
jq, save the hook scripts and runchmod +xon them. - Add a Stop hook (Claude Code) or a stop hook (Cursor) that runs the typecheck and the fast tests.
- Add the per-file lint hook if your linter is fast.
- Connect a scanner the agent can call, or add Gitleaks and Trivy to the Stop hook.
- Give the agent a small task that breaks a test, and watch the hook stop it from finishing. Now you know the gate works.
Before launch, add the checks no hook can run for you, from our vibe coding security checklist.
FAQ
Can Claude Code run tests automatically before it finishes?
Yes. A Stop hook runs when Claude tries to end its turn. If the hook returns a block decision with a reason, for example because the tests failed, Claude keeps working and sees the reason. Check the stop_hook_active field in the hook input so the hook does not block forever.
Does Cursor support hooks?
Yes. Cursor reads hooks from .cursor/hooks.json in the project and ~/.cursor/hooks.json for your user. Its stop hook can return a followup_message, which Cursor sends as the next message so the agent continues. Cursor limits these follow-ups to 5 per script by default.
Should I use CLAUDE.md or AGENTS.md?
If you use both Cursor and Claude Code, AGENTS.md covers both. Claude Code reads AGENTS.md only when the repository has no CLAUDE.md, so if you keep a CLAUDE.md, import AGENTS.md from it with the line @AGENTS.md.
Will hooks slow my agent down?
Every check adds its runtime to the event it runs on. Keep per-edit hooks to a single-file lint, put the typecheck and fast tests in the stop hook, and leave the full test suite to CI.
Do I still need code review if the agent checks its own code?
Yes, for authorization and intent. Hooks and scanners prove the code compiles, passes your tests and avoids known insecure patterns. They do not know which user should see which record, and that kind of bug is where human review, or a model-based review such as /security-review, earns its time.