Claude Code Diff Verification in 2026: How to Make Your Agent Assert Its Own Edits Did Not Break the Build
This article was written with the assistance of AI, under human supervision and review.
Developers who ship code written by Claude Code in 2026 face a gap between what the agent claims it did and what actually changed. The agent generates edits, reports success, and stops. The build might be broken, tests might fail silently, or type errors might sit unnoticed until you run the check yourself. The failure mode looks like this: Claude edits three files, says the feature is done, and you merge it into main before discovering the auth middleware now crashes on every request because a required field is missing.
Problem flow showing Claude Code reporting success while build is broken
The Claude Code documentation states that Claude stops when the work looks done, and if it has nothing to run that tells it whether the work actually is done, it has no way to verify its own changes. The solution is a post-edit verification hook that runs git diff, tests, and type checks before the agent finishes its turn. When the hook exits with code 2, Claude Code sends the errors back to Claude, which fixes the problem and runs the check again.
Solution flow showing Stop hook blocking turn end until typecheck passes
This post shows how to build verification into every agent session so the agent asserts its own edits before you approve them.
Key Takeaways
- Claude Code cannot verify its own changes without an executable check (tests, type check, build command, or linter) that returns pass or fail.
- A Stop hook that exits with code 2 blocks the turn from ending and sends errors back to Claude, forcing it to fix the problem before claiming success.
- The verification pattern has four layers: in-prompt requests (agent decides when satisfied),
/goalconditions (checked after every turn), Stop hooks (enforced by the harness), and independent subagent review. - Manual review after the fact catches what hooks miss, but hook-based verification catches what manual review skips because the diff looked small.
- Production workflows combine pre-commit hooks (format, lint), Stop hooks (typecheck, unit tests), and CI (integration tests, security scans) at different checkpoints in the pipeline.
Understanding What Claude Code Can and Cannot Verify on Its Own
Claude Code has access to every tool a developer has. It can run git diff, execute test suites, and read compiler output. The gap is not in capability but in awareness. When Claude finishes editing, it decides the turn is done based on whether the work matches the prompt. Without an executable check that returns a pass or fail signal, the agent has no feedback loop that tells it whether the code actually works.
Claude Code verification capabilities showing what it can read versus what it can enforce
The overview documentation explains that Claude Code reads your codebase, edits files, runs commands, and integrates with development tools. Reading and running are not the problem. The problem is that running a check is optional from the agent's perspective unless the check is mandatory in the workflow. A developer who asks Claude to "add the rate limiter and make sure tests pass" gets an agent that decides for itself when it is satisfied. If the tests pass on the first try, great. If the agent thinks they passed but did not read the full output, the session ends with failing tests.
The four verification layers differ in who decides when the work is done. At the weakest layer, the agent decides based on whether it thinks it satisfied the prompt. At the strongest layer, the harness (Claude Code itself) decides based on whether a script exits with code 0. The layers are:
| Layer | Decision maker | When it runs | Override mechanism |
|---|---|---|---|
| In-prompt request | Agent | When agent chooses | Agent decides satisfaction |
/goal condition | Separate model | After every turn | Keeps agent working until condition met |
| Stop hook | Claude Code harness | Before turn ends | Exit code 2 blocks stop |
| Subagent review | Independent subagent | On demand | Reports findings back to main session |
The in-prompt request is the baseline. Asking Claude to "run tests after and keep going until they pass" works for any task today with no setup. The agent runs the tests, reads the output, and decides whether to keep going. This catches obvious failures but relies on the agent's judgment about what output means success.
A /goal condition sets a target that a separate model checks after every turn. The best practices documentation states that goals matter for long background runs, where Claude sometimes ends a turn with a progress summary while items are still open, and the goal sends it back. The condition is checked by a model, not a script, so it interprets natural language criteria like "all tests in tests/auth pass and lint is clean."
A Stop hook runs a shell command whenever Claude tries to end its turn. If the command exits with code 2, the turn does not end. The hook is enforced by the Claude Code harness, so the typecheck has to pass before the session can finish. This is the layer where verification becomes mandatory rather than requested.
A subagent reviewer gets a fresh conversation with no memory of how the code was written. The code review documentation describes /code-review, which reviews the diff in a subagent that never saw your conversation. The subagent is not swayed by the reasoning that produced the code, so it catches mistakes the main agent might rationalize away.
Verification layers diagram
The gap in awareness happens because the agent treats "run the tests" as a tool call, not as a verification gate. When you ask Claude to run tests, it executes the command, reads the output, and continues based on what it sees. If the output is long and the agent decides to summarize it rather than reading every line, it might miss a failure buried in the middle. The Stop hook closes this gap by making the check mandatory. The agent cannot end the turn until the script exits with code 0.
Building a Post-Edit Verification Hook That Runs git diff, Tests, and Type Checks
A Stop hook is a shell command defined in .claude/settings.json that runs before Claude Code allows a turn to end. If the command exits with code 0 or 1, the turn ends normally. If it exits with code 2, Claude Code sends the command's stderr back to Claude and blocks the stop. The agent gets the error output and continues working until the check passes.
The minimal Stop hook that enforces type checking looks like this:
{ "hooks": { "Stop": [ { "type": "command", "command": "pnpm tsc --noEmit 1>&2 || exit 2" } ] } } The command runs pnpm tsc --noEmit and redirects stdout to stderr with 1>&2 so all output goes to stderr. If tsc fails, the command exits with code 2, which tells Claude Code to block the stop and send the compiler errors back to Claude. Claude reads the errors, fixes the type issues, and the Stop hook runs again. This loop continues until tsc exits with code 0.
The hooks documentation states that if the hook blocks eight stops in a row with no tool call between them, Claude Code overrides the next block and ends the turn. The default cap is eight. To raise it, set the CLAUDE_CODE_STOP_HOOK_BLOCK_CAP environment variable.
A more complete Stop hook that combines type checking, linting, and unit tests looks like this:
// .claude/verify.sh #!/bin/bash set -e echo "Running verification checks..." >&2 # Type check pnpm tsc --noEmit 2>&1 | tee >(cat >&2) if [ ${PIPESTATUS[0]} -ne 0 ]; then echo "Type check failed" >&2 exit 2 fi # Lint pnpm eslint src --max-warnings 0 2>&1 | tee >(cat >&2) if [ ${PIPESTATUS[0]} -ne 0 ]; then echo "Lint failed" >&2 exit 2 fi # Unit tests pnpm test:unit 2>&1 | tee >(cat >&2) if [ ${PIPESTATUS[0]} -ne 0 ]; then echo "Unit tests failed" >&2 exit 2 fi echo "All checks passed" >&2 exit 0 The script runs three checks in sequence and exits with code 2 if any fails. The tee >(cat >&2) construct sends output to both stdout (for the tee) and stderr (for Claude Code), so Claude sees the full output when a check fails. The script exits early on the first failure, so Claude gets immediate feedback about what broke without waiting for the remaining checks.
The settings file references the script:
{ "hooks": { "Stop": [ { "type": "command", "command": "bash .claude/verify.sh" } ] } } The Stop hook runs every time Claude tries to end a turn, so it catches mistakes before you see the changes. When Claude edits a file and introduces a type error, the Stop hook blocks the stop, sends the compiler error back, and Claude fixes it. The fix might introduce a lint warning, which blocks the next stop and sends Claude back again. This loop continues until all three checks pass.
The cost of this approach is token usage. Every blocked stop sends the full error output back to Claude, which extends the conversation. A type error that takes three attempts to fix adds three error messages to the context. For short error messages this is negligible. For long test output or verbose compiler errors, it can add thousands of tokens per session. The best practices documentation recommends asking for short summaries back when subagents run long checks, and the same principle applies to Stop hooks.
The verification script can also run git diff to show Claude what changed. A diff check that looks for common mistakes might look like this:
// .claude/diff-check.sh #!/bin/bash DIFF=$(git diff HEAD) if echo "$DIFF" | grep -q "TODO"; then echo "Diff contains TODO markers" >&2 exit 2 fi if echo "$DIFF" | grep -q "console.log"; then echo "Diff contains console.log statements" >&2 exit 2 fi if echo "$DIFF" | grep -q "debugger"; then echo "Diff contains debugger statements" >&2 exit 2 fi exit 0 This script fails the Stop hook if the diff contains TODO markers, console.log statements, or debugger statements. The check catches debugging code that should have been removed before the session ended. The script is simplistic (it flags legitimate uses of console.log in logging utilities), but it demonstrates the pattern. A production version would use a proper parser or limit the check to specific directories.
The Stop hook is a blunt instrument. It blocks every stop attempt, which means it blocks even when Claude is legitimately done and you want to review the changes yourself. The settings reference does not document a way to disable the Stop hook mid-session, so the workaround is to exit the session or temporarily modify .claude/settings.json. For workflows where you want manual review before auto-fixing, the Stop hook might be too aggressive.
Assertion Patterns: Making the Agent Verify Its Own Changes Before You Approve
The verification pattern has four checkpoints where the agent can assert its own changes. The checkpoints differ in what they cost and what they catch. Combining them gives you defense in depth: the in-prompt request catches obvious breaks, the Stop hook enforces correctness, and the independent review catches logic errors the tests missed.
Verification pattern execution flow showing progression from edit through multiple checks
The first checkpoint is the in-prompt request. When you ask Claude to "add pagination to the search endpoint and verify the tests pass," Claude makes the changes, runs the test suite, reads the output, and reports what it saw. This catches failures that produce clear error messages. It does not catch failures where the agent misinterprets the output or decides a warning is not worth fixing.
The second checkpoint is the /goal condition. Setting a goal with /goal all unit tests pass and no lint warnings gives Claude a target that a separate model checks after every turn. The best practices documentation explains that goals matter for background sessions, where Claude might end a turn early while work is still pending. The goal model sees the conversation and the goal statement and decides whether the goal is met. If not, Claude continues.
The third checkpoint is the Stop hook. The hook enforces the check at the harness level, so the agent cannot bypass it. A Stop hook that runs pnpm tsc --noEmit 1>&2 || exit 2 blocks every stop attempt until the typecheck passes. The agent sees the compiler errors and fixes them before you see the changes. This checkpoint catches type errors, lint violations, and test failures that the agent might have missed or misinterpreted.
The fourth checkpoint is an independent review. Running /code-review in the session sends the diff to a fresh subagent that reviews the changes without having seen how they were written. The subagent looks for logic errors, missing edge cases, and security issues that tests and type checks do not catch. The review comes back into your session, where Claude can fix the problems before you approve the changes.
The pattern that catches the most problems combines all four checkpoints:
- Ask Claude to "implement the feature and verify tests pass" (in-prompt request)
- Set
/goal feature complete, all tests pass, no type errors(automatic goal check) - Add a Stop hook that runs typecheck and lint (enforced by harness)
- Run
/code-reviewbefore merging (independent perspective)
Each checkpoint has a different failure mode. The in-prompt request fails when the agent skips the check or misinterprets the output. The goal check fails when the goal statement is ambiguous or the goal model is too lenient. The Stop hook fails when the script itself has bugs or when the check is too strict (blocking legitimate work). The independent review fails when the subagent lacks context or when the diff is too large to review effectively.
The cost-benefit tradeoff differs by project. A small prototype where you are the only developer might use only the in-prompt request. A production codebase with multiple contributors might use all four checkpoints. The Stop hook has the best cost-to-value ratio: it costs almost nothing to set up (one file in .claude/settings.json) and it catches every type error and test failure before you see the changes.
The assertion pattern also applies to changes you make yourself. A Stop hook that runs on every turn also runs when you edit a file manually and then ask Claude to continue. The hook catches your mistakes as well as Claude's mistakes. This makes the Stop hook a form of continuous verification that runs throughout the session, not just when Claude is working.
Comparison: Manual Review vs Hook-Based Verification vs Full CI Integration
The three approaches to verifying agent-written code differ in when they catch problems, what they cost, and how much they trust the agent. Manual review catches problems after the session ends. Hook-based verification catches problems during the session before you see the changes. Full CI integration catches problems after you merge but before production.
Comparison of verification approaches showing when each catches problems
Manual review happens after the session ends. You run git diff, look at the changes, and check for problems. This approach catches mistakes that automated checks miss: logic errors, missing edge cases, and changes that work but do not match the requirements. The failure mode is that you skip the review because the diff looks small, or you skim it and miss a subtle bug. Manual review also happens after the agent session, so fixing problems means reopening the conversation or making the fix yourself.
| Approach | Catch time | Cost | What it catches | What it misses |
|---|---|---|---|---|
| Manual review | After session | Developer time | Logic errors, requirements mismatch | Skipped reviews, subtle bugs |
| Hook verification | During session | Token usage for fixes | Type errors, test failures, lint | Logic errors, integration issues |
| Full CI | After merge | CI compute time | Integration failures, security | Problems caught too late |
Hook-based verification happens during the session before you see the changes. The Stop hook runs checks and blocks the turn from ending until they pass. This approach catches type errors, test failures, and lint violations before you start the review. The failure mode is that the hook is too strict (blocking legitimate work) or too lenient (missing problems that matter). Hook verification also costs tokens: every blocked stop sends error output back to Claude, extending the conversation.
Full CI integration happens after you merge the code. The CI pipeline runs the complete test suite, integration tests, and security scans. This approach catches problems that unit tests and type checks miss: integration failures, environment-specific bugs, and security vulnerabilities. The failure mode is that problems are caught late, after you already approved the changes and merged them to main. Fixing a CI failure means reverting the merge or opening a new PR.
The three approaches are not mutually exclusive. A complete verification workflow combines them at different checkpoints:
- Stop hook runs during the session (type check, lint, unit tests)
- Manual review happens before merge (logic, requirements, security)
- CI runs after merge (integration tests, E2E tests, security scans)
The Stop hook catches fast, cheap checks that can run in seconds. Manual review catches problems that need human judgment. CI catches expensive checks that need a clean environment or external services. Each layer catches what the previous layer missed.
The Claude Code CI documentation covers how to integrate Claude Code with GitHub Actions and GitLab CI/CD for automated PR reviews and issue triage. The CI integration runs Claude Code in a fresh environment with no session history, so it catches problems that the local session missed because of stale state or local configuration.
The cost structure differs by approach. Manual review costs developer time. A five-minute review per PR adds up when you ship ten PRs per day. Hook verification costs tokens. A Stop hook that blocks three times and sends 500 tokens of error output back each time adds 1,500 tokens to the session. CI costs compute time. Running the full test suite on every merge takes minutes and costs money on hosted CI services.
The tradeoff is between catching problems early (when they are cheap to fix) and catching problems thoroughly (when you have complete information). A Stop hook that runs unit tests catches problems in seconds, but it might miss integration issues that only show up when the feature runs in production. CI catches integration issues, but by then you already merged the code and the fix is a new PR instead of an iteration in the same session.
Production Patterns: Pre-Commit Checks, Test Suites, and What to Auto-Accept
Production workflows layer verification checks at different points in the development pipeline. Pre-commit hooks run before code enters version control. Stop hooks run during agent sessions. CI runs before code reaches production. Each layer has different criteria for what to check and what to auto-accept.
Production verification pipeline showing checks at each stage
Pre-commit hooks run before git commit succeeds. A typical pre-commit hook runs formatters (Prettier, Black), linters (ESLint, Pylint), and static analysis. These checks are fast (under one second) and deterministic (they always produce the same result for the same input). Pre-commit hooks are auto-fixing: if the formatter changes a file, the commit includes the formatted version. The hook fails only if the linter finds an error the formatter cannot fix.
Stop hooks run during Claude Code sessions before the agent can end a turn. A typical Stop hook runs type checks (tsc --noEmit, mypy), unit tests (pnpm test:unit, pytest tests/unit), and linters. These checks are slower than pre-commit hooks (seconds to tens of seconds) but still fast enough to run on every stop attempt. Stop hooks are not auto-fixing: they send errors back to Claude, which fixes the code and tries again.
CI runs after code merges to a branch or opens a PR. A typical CI pipeline runs integration tests, end-to-end tests, security scans (Snyk, Dependabot), and deployment to staging. These checks are slow (minutes to tens of minutes) and might depend on external services (databases, APIs, cloud resources). CI is blocking: a failed CI run prevents the PR from merging or the code from deploying.
The pattern that works in production is:
- Pre-commit: format, lint (auto-fixing, under one second)
- Stop hook: typecheck, unit tests (agent-fixing, under ten seconds)
- CI: integration tests, security scans, staging deploy (blocking, minutes)
Each layer checks what the previous layer cannot catch or cannot afford to run. Pre-commit hooks catch formatting and obvious lint errors. Stop hooks catch type errors and unit test failures. CI catches integration issues and security vulnerabilities.
Production pipeline diagram
What to auto-accept depends on the risk of the change and the cost of being wrong. A formatting change has low risk (it does not change behavior) and low cost (you can revert it in one commit), so auto-accepting formatted code is safe. A database migration has high risk (it can lose data) and high cost (reverting requires a rollback), so auto-accepting migrations is dangerous.
The auto-accept criteria differ by check:
| Check | Auto-accept? | Why |
|---|---|---|
| Prettier format | Yes | Deterministic, no behavior change |
| ESLint auto-fix | Yes | Simple transforms, rarely breaks code |
| Type errors fixed by agent | Conditional | Review if change is large or risky |
| Unit tests passing | No | Passing tests do not mean correct behavior |
| Integration tests passing | No | Tests might pass with wrong behavior |
The conditional case (type errors fixed by agent) depends on the scope of the change. If Claude fixes a type error by adding a type annotation, that is low risk and can auto-accept. If Claude fixes a type error by refactoring a function, that is higher risk and needs review. The Stop hook does not distinguish between these cases, so the review step happens manually.
The tests-passing case is subtle. A Stop hook that blocks until unit tests pass does not guarantee the code is correct. It only guarantees the tests passed. If the tests are incomplete or if the tests pass for the wrong reason, the Stop hook lets bad code through. The independent review checkpoint catches this: a subagent that reviews the diff can spot missing test cases even when existing tests pass.
A realistic Stop hook configuration for a TypeScript backend might look like this:
{ "hooks": { "Stop": [ { "type": "command", "command": "pnpm tsc --noEmit 1>&2 || exit 2" }, { "type": "command", "command": "pnpm test:unit --passWithNoTests 1>&2 || exit 2" } ] } } This configuration runs two checks: tsc --noEmit for type checking and pnpm test:unit for unit tests. The --passWithNoTests flag makes the test command succeed even if no tests ran, which prevents the Stop hook from blocking when you are working on a feature that does not have tests yet. Both commands redirect output to stderr so Claude sees the full error messages when a check fails.
The cost control documentation covers how to manage token usage in production workflows where Stop hooks run frequently. The main cost-control pattern is to keep Stop hook checks fast and focused: run type checks and unit tests, but not integration tests or security scans. Save the expensive checks for CI, where they run once per merge instead of once per stop attempt.
The verification pattern also applies to changes that are not code. A Stop hook that runs markdownlint catches formatting errors in documentation. A Stop hook that runs jsonlint catches syntax errors in config files. The pattern is the same: define a check that returns pass or fail, put it in a Stop hook, and let the agent fix failures before the session ends.
Frequently Asked Questions
What happens if the Stop hook blocks eight times in a row?
Claude Code overrides the block and ends the turn. The default cap is eight blocked stops with no tool call between them. Set CLAUDE_CODE_STOP_HOOK_BLOCK_CAP higher if you need more attempts, but if the agent cannot fix the problem in eight tries, the issue is usually with the check itself (too strict) or the task (too complex for the agent).
Can I disable the Stop hook for one session without editing settings.json?
The hooks documentation does not document a way to disable a Stop hook mid-session. The workaround is to comment out the hook in .claude/settings.json, restart the session, or use a different worktree that does not have the hook configured. For workflows where you want manual control, skip the Stop hook and use in-prompt requests instead.
Should the Stop hook run the full test suite or only unit tests?
Run only fast tests (under ten seconds). A Stop hook that takes 30 seconds to run blocks the agent for 30 seconds on every stop attempt, which adds up to minutes of wait time per session. Save slow tests (integration tests, E2E tests) for CI. The best practices documentation recommends running checks that come back pass or fail quickly.
Does the Stop hook work with subagents and background sessions?
Yes. The Stop hook runs in every session, including subagent sessions and background sessions. A subagent that edits files and tries to stop gets blocked by the same Stop hook the main session uses. This means background tasks self-validate before finishing, so you can trust the results without reviewing every session individually.
What if the Stop hook itself has a bug?
The session hangs. If the Stop hook script has a syntax error or references a command that does not exist, every stop attempt fails and Claude cannot end the turn. The fix is to exit the session, fix the script in .claude/settings.json, and restart. Test Stop hook scripts outside Claude Code first (run bash .claude/verify.sh yourself) to catch script bugs before they block a session.
Conclusion: Trust but Verify
The gap between what Claude Code claims and what actually works closes when the agent can run the same checks a developer would run. A Stop hook that enforces type checking, linting, and unit tests turns agent sessions into self-validating workflows. The agent makes changes, the hook blocks the turn until checks pass, and the agent fixes failures before you see the changes. What you get is code that passed verification before it reached your review, which means the review focuses on logic and requirements instead of catching type errors and test failures.
The production pattern layers verification at three checkpoints: pre-commit hooks that auto-fix formatting, Stop hooks that agent-fix correctness, and CI that blocks bad merges. Each layer catches what the previous layer cannot afford to run. The cost is token usage for Stop hook iterations and CI compute time for integration tests. The payoff is code that self-validates as it is written instead of breaking when you run it.
Start with a Stop hook that runs tsc --noEmit 1>&2 || exit 2 in .claude/settings.json. Add unit tests to the hook once you have a fast test suite. Save integration tests and security scans for CI. The verification pattern works because it gives Claude something executable that returns pass or fail, and that executable check is what closes the awareness gap.








