# Verification Gates for Agents

Course: [Testing & Verification](https://www.devtutorials.ai/learn/testing-verification) · Section: Verification as a System
Source: https://www.devtutorials.ai/learn/testing-verification/verification-system/verification-gates

> Turn rules into gates. See how an agent fakes green, build a test-diff guard that blocks skipped, loosened and deleted tests, write the definition of done as code with clear reasons, and wire the gates into Claude Code, Codex, Cursor and CI.

## From Rules to Gates

So far, **you** checked the evidence. That works for one change. It does not work for fifty changes a day.

Agents can also fake green. When a test goes red, an agent may:

- add `.skip` to the test
- loosen the check, like `toBe(4800)` → `toBeDefined()`
- delete the test

A rule in `CLAUDE.md` or `AGENTS.md` says "do not do this". The agent usually listens. But "usually" is not enough.

**Gate** = an automatic check that **blocks** a change until the evidence is there.

In this lesson, you will build two gates in code, then wire them into your agent and your CI.

**Simple lesson:** rules guide the agent. Gates stop it.

### A Real-World Example

Think about a metro station.

- A sign says "Please buy a ticket." That is a rule. Most people follow it.
- The ticket barrier only opens with a valid card. That is a gate. Nobody gets through without one.

Your verification pack has both. The signs are the policy lines. The barriers are the hooks and CI checks.

## What You Will Learn

In this lesson, you will learn:

1. How agents fake green, and why rules alone do not stop it.
2. How to build a **test-diff guard** that blocks skipped, loosened and deleted tests.
3. How to write a **definition of done** as code.
4. Where gates live in Claude Code, Codex, Cursor and CI.
5. How to export the final pack with every gate.

## How Agents Fake Green

Press **▶ Run tests**.

- **The Tests tab:** "1 passed | 1 skipped". Green.
- **`AI_SUMMARY.md`:** "Added the ₹200 limit. All tests pass ✅".
- **The truth:** the limit was never added. The agent skipped the test that showed it.
- **Now try:** remove `.skip` and run again. Red: 4500, not 4800.

**This is not the agent being sneaky.** Its goal was "make the tests pass". Skipping a red test does that. A gate changes the goal to "make the tests pass without touching the checks".

**Simple lesson:** if a shortcut to green exists, an agent will sometimes take it. Close the shortcut.

## Gate 1: The Test-Diff Guard

### What It Does

**Test-diff guard** = a script that reads the diff of the test files and blocks:

- added `.skip`, `.only` or `.todo`
- removed `expect(...)` checks
- exact checks changed to loose ones

It must also let real work through:

- a real code fix
- a new test
- a check that was moved, not removed

### Build It

The starter guard only finds `.skip(`. The tests show what else it must do.

Press **▶ Run tests**.

- **The Tests tab:** red for `.only`, the loosened check and the deleted test.
- **Open `src/guard.ts`** and improve it. Three ideas:
    - Only look at test files: names with `.test.` or `.spec.`.
    - Collect added (`+`) and removed (`-`) lines.
    - A removed line with `expect(` is a problem, unless the same line was added back.
- **Run again** until all seven tests are green.

### The Guard Against Four Agent Moves

Here is a finished guard. A simulated agent tries four moves after a red test.

Press **▶ Run tests**. Green.

- **The real fix:** allowed.
- **Skip, loosen and delete:** all blocked.

<figure className="ver-fig">
<svg viewBox="0 0 640 210" role="img" aria-labelledby="ver-fig-gate" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="ver-fig-gate">Animation: four agent changes move toward a gate. Skip, loosen and delete bounce back in red. The real fix passes through in green.</title>
  <rect x="300" y="20" width="40" height="170" rx="6" style={{ fill: "var(--ver-slate-soft)", stroke: "var(--ver-slate)", strokeWidth: 2 }} />
  <text x="320" y="200" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>Gate</text>
  <g style={{ fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>
    <g>
      <rect y="30" width="110" height="28" rx="6" style={{ fill: "var(--ver-red-soft)", stroke: "var(--ver-red)" }}><animate attributeName="x" values="20;180;20" keyTimes="0;0.4;1" dur="6s" repeatCount="indefinite" /></rect>
      <text y="49" style={{ fill: "var(--ver-red)" }}>add .skip<animate attributeName="x" values="30;190;30" keyTimes="0;0.4;1" dur="6s" repeatCount="indefinite" /></text>
    </g>
    <g>
      <rect y="70" width="110" height="28" rx="6" style={{ fill: "var(--ver-red-soft)", stroke: "var(--ver-red)" }}><animate attributeName="x" values="20;180;20" keyTimes="0;0.45;1" dur="6s" begin="0.4s" repeatCount="indefinite" /></rect>
      <text y="89" style={{ fill: "var(--ver-red)" }}>loosen check<animate attributeName="x" values="30;190;30" keyTimes="0;0.45;1" dur="6s" begin="0.4s" repeatCount="indefinite" /></text>
    </g>
    <g>
      <rect y="110" width="110" height="28" rx="6" style={{ fill: "var(--ver-red-soft)", stroke: "var(--ver-red)" }}><animate attributeName="x" values="20;180;20" keyTimes="0;0.5;1" dur="6s" begin="0.8s" repeatCount="indefinite" /></rect>
      <text y="129" style={{ fill: "var(--ver-red)" }}>delete test<animate attributeName="x" values="30;190;30" keyTimes="0;0.5;1" dur="6s" begin="0.8s" repeatCount="indefinite" /></text>
    </g>
    <g>
      <rect y="150" width="110" height="28" rx="6" style={{ fill: "var(--ver-green-soft)", stroke: "var(--ver-green)" }}><animate attributeName="x" values="20;500;500" keyTimes="0;0.6;1" dur="6s" repeatCount="indefinite" /></rect>
      <text y="169" style={{ fill: "var(--ver-green)" }}>fix the code<animate attributeName="x" values="30;510;510" keyTimes="0;0.6;1" dur="6s" repeatCount="indefinite" /></text>
    </g>
  </g>
</svg>
<figcaption>The guard blocks the three shortcuts and lets the real fix through.</figcaption>
</figure>

**Simple lesson:** a good gate blocks the shortcuts and lets real work through.

## Gate 2: The Definition of Done

**Definition of done** = the list of evidence a change needs before anyone can call it done.

Written as code, the agent cannot argue with it:

- no type errors
- at least one test ran, and none failed
- the new tests failed on the old code first
- the mutation score is at or above the threshold
- the test-diff guard found no problems

Press **▶ Run tests**. Green. Read `src/done.ts`. Each rule adds a **reason** when it is not met.

Now ask the gate about a change where the agent said "done".

Press **▶ Run tests**.

- **The Tests tab:** red. The error lists three reasons: no fail-first, a mutation score of 55%, and a guard problem.
- **Why reasons matter:** the agent gets told exactly what is missing, so it can fix it.

**Give the agent the reasons, not just "no".** A hook that prints "mutation score 55% is below 80%" helps the agent write better tests. A hook that only fails makes it guess.

**Simple lesson:** "done" means the evidence is there, and a script can check it.

## Where the Gates Live

The ideas are the same in every tool. Only the file changes.

| Piece | Claude Code | Codex | Cursor |
|---|---|---|---|
| Test policy (rule) | `CLAUDE.md` | `AGENTS.md` | `AGENTS.md` or `.cursor/rules` |
| Review checklist (rule) | `.claude/skills/` | `.agents/skills/` | `.cursor/skills/` |
| Guard after each edit (gate) | PostToolUse hook in `.claude/settings.json` | Hook in `.codex/hooks.json` | `.cursor/hooks.json` |
| Mutation gate (gate) | Hook or CI | Hook or CI | Hook or CI |
| Merge gate | GitHub Actions or Azure Pipelines | Same | Same |

- **Hooks** run right after the agent edits a file. The agent sees the reason and tries again.
- **CI** runs on every pull request. It is the last barrier, for people and agents alike.

**Hook formats change.** The table and the pack follow each tool's docs as of November 2026. Check your tool's hooks page when you install the pack.

### The Final Pack

This pack adds the mutation gate hook to the v1 pack from **User-Flow Checks**.

**Simple lesson:** rules in the rules file, gates in hooks and CI, the same ideas in every tool.

## TL;DR

1. **Gate** = an automatic check that blocks a change until the evidence is there.
2. **Rule** = guidance the agent reads. Usually followed, never forced.
3. **Faking green** = skipping, loosening or deleting a test to get green.
4. **Test-diff guard** = blocks `.skip`, `.only`, `.todo`, removed checks and loosened checks. Lets real work through.
5. **Definition of done** = types clean, tests pass, fail-first, mutation score, guard clean. Each missing item is a reason.
6. **Where gates live:** hooks after each edit, CI before each merge.

## Common mistakes

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Rules only | "The rules file says never skip tests." | Add a hook that blocks it. | One day the agent skips a test anyway. |
| A guard that blocks too much | "Block every removed line." | Only test files, and allow moved checks. | Real fixes are blocked, and people turn the guard off. |
| "No" without reasons | "The hook just exits with an error." | Print what is missing. | The agent guesses and loops. |
| `<=` in the threshold | "Below or equal to 80 fails." | Exactly the threshold passes. | Good changes are blocked. |
| Gates in hooks only | "The hook is enough." | Also run them in CI before merge. | A change made outside the agent skips every check. |

## Check Your Understanding

## Exercises

## What to Remember

- Rules guide. Gates block.
- Agents may fake green by skipping, loosening or deleting tests.
- A test-diff guard blocks the shortcuts and lets real work through.
- Write the definition of done as code, with clear reasons.
- Run gates in hooks after each edit, and in CI before each merge.

## Further Reading

- [Claude Code hooks](https://docs.claude.com/en/docs/claude-code/hooks): PostToolUse and other hook events.
- [Stryker incremental mode](https://stryker-mutator.io/docs/stryker-js/incremental/): run mutation testing on changed code only.
- [GitHub protected branches](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches): require checks before merge.

Next: **Characterization Tests**. You will pin what old code does today, so an AI refactor cannot quietly change it.
