Kousigan A

A checklist the agent can skip: turn it into quality gates

Turn your definition of done into gates that block: agent hooks, pre-commit, required CI checks and rulesets, and which failures still need a person.

Last verified against official docs on

An AI coding agent can skip a definition-of-done checklist for AI-generated code. Turn each item into a quality gate: an agent hook, a pre-commit hook, a required CI check, or a branch rule that stops the merge. Then decide which failures block, which only warn, and which need a person.

Part 4 of this series ended with that checklist. This part enforces it. A gate doesn’t prove the code is right. Green CI means the checks you wrote passed, not that the requirement is met. What a gate does is stop the known misses before they reach your main branch.

Claude Code is the worked example. The tool behaviour below comes from each tool’s official docs and GitHub’s docs, last verified on 11 October 2026. None of it was tested for this post.

Five gates between a coding agent and your main branch

Each gate runs at a different point, and each one is skipped in a different way:

Gate Where it runs What it stops How it gets skipped
Agent hook Inside the agent session The agent finishing, or one command An edit to the hook settings, or a session without them
Pre-commit hook (Git docs) On git commit, on the machine The commit git commit --no-verify
Required status check (GitHub docs) In CI, on the pull request The merge An admin, unless bypassing is turned off
Code scanning results (GitHub docs) In CI, through a ruleset The merge, for alerts at the severity you pick Anyone on the ruleset’s bypass list
Code owner review (GitHub docs) On the pull request The merge, until an owner approves An admin, unless bypassing is turned off

The early gates give the agent fast feedback while it still has the context. The late ones are much harder to get past. The Codex hooks docs say: “Treat tool hooks as a useful guardrail, not a complete enforcement boundary.” So the agent hook is the first gate, and CI is the last.

Run the same commands at every layer, so a pass on the laptop means a pass in CI. Part 1 covers why a hook beats a line in the rules file. Part 3 covers why a hook is a guarantee and a skill is a request.

Which checks block, which warn, and which need a person

Each item in Part 4’s definition of done gets a gate and a mode:

Definition-of-done item Gate Mode
Tests passed Agent finish hook, then a required check Block
Lint passed Pre-commit hook, then a required check Block
Type checks passed Pre-commit hook, then a required check Block
Build passed Required check Block
Security implications considered Code scanning results in a ruleset Block at the severity you choose; a person for the rest
Tests added/updated A coverage report in CI Warn; a person judges whether the tests prove the behaviour
Final diff reviewed, unintended changes checked Code owner review on sensitive paths Person
Acceptance criteria verified The pull request review Person
Remaining uncertainty documented The pull request description Person
Human review completed where necessary Required approvals Block on the approval; the review itself is a person

The rest of the list, such as understanding the requirement and thinking through edge cases, has no gate. It is the agent’s work and your review.

A common way to choose the mode: block on a check that is deterministic, fast and almost never wrong. Lint, types, the build and a stable test suite fit that. A noisy or slow check, or one that tracks a trend like bundle size, works better as a warning. A gate that fails for no reason teaches everyone to work around it. Anything that depends on intent or risk goes to a person.

In GitHub Actions, one job can hold both kinds. An example workflow, assuming your project has these npm scripts and uses Jest for tests:

.github/workflows/checks.yml
# Example: one blocking job, with a warn-only step at the end
name: checks
on: pull_request
jobs:
  gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test -- --ci
      - run: npm run build
      - name: Bundle size (warn only)
        run: npm run size
        continue-on-error: true

Make gate a required status check and it blocks the merge. The last step can fail without failing the job: GitHub’s workflow syntax says continue-on-error “Prevents a job from failing when a step fails”. A warning still needs a reader, so put it where reviewers look.

A finish hook that exits with code 1 may not stop anything

An agent finish hook runs inside the agent session. It runs the tests when the agent tries to finish, and sends it back if they fail. Claude Code’s hooks reference says a Stop hook that exits with code 2 “Prevents Claude from stopping, continues the conversation”.

The trap is the exit code. A failing test run exits with a non-zero code, usually 1. The same reference says: “Without valid JSON on stdout, Claude Code treats exit code 1 as a non-blocking error, even though 1 is the conventional Unix failure code. If your hook is meant to enforce a policy, use exit 2.” So a Stop hook that only runs npm test shows an error and lets Claude stop anyway. The script has to turn a failure into exit code 2. An example:

.claude/hooks/require-tests.sh
#!/bin/bash
# Example: send Claude back once if the tests fail.
INPUT=$(cat)

# Already sent back once: let it stop and report, instead of looping.
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi

if ! npm test 1>&2; then
  echo "Tests are failing. Fix the code, not the tests, then finish." >&2
  exit 2
fi
exit 0

Then register the example script in .claude/settings.json:

.claude/settings.json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/require-tests.sh"
          }
        ]
      }
    ]
  }
}

Per the hooks guide, Claude Code overrides a Stop hook after it blocks eight times in a row with no tool call in between. The guide’s answer is the stop_hook_active check: exit early once the script has already sent Claude back. Here that means one retry, then a report, while CI still holds the line. Part 9 covers why retries need a limit.

The same guide says Stop hooks fire “whenever Claude finishes responding, not only at task completion”, so keep the command fast. Per the hooks reference, a hook that can’t start at all, such as a mistyped script path, is also a non-blocking error. To make a broken hook block, set onFailure: "block", which needs Claude Code v2.1.295 or later.

How a hook blocks in six coding agents

Each coding agent draws the line in a different place:

Agent Send it back when it tries to finish Block a command before it runs When the hook script itself fails
Claude Code (hooks) Stop exits 2, or returns a block decision PreToolUse exits 2, or returns permissionDecision: "deny"; this holds even in bypassPermissions mode Exit 1 or a crash is a non-blocking error and the action goes ahead, unless onFailure: "block"
OpenAI Codex (hooks) Stop returns "decision": "block" or exits 2; the reason becomes the next prompt PreToolUse denies Bash, apply_patch edits and MCP calls An unsupported output field marks the hook as failed and the tool call goes ahead
Cursor (hooks) A stop hook returns followup_message, sent as the next user message; five loops by default (loop_limit) beforeShellExecution or preToolUse returns deny, or exits 2 Fail-open: crashes, timeouts and other exit codes let the action through, unless failClosed: true
Gemini CLI (hooks reference) AfterAgent returns "decision": "deny" or exits 2; the response is rejected and retried BeforeTool returns deny or exits 2 Any other exit code is a warning and the CLI continues
GitHub Copilot, cloud agent and CLI (hooks reference) agentStop returns "decision": "block"; exit 2 on this event is only a warning preToolUse returns deny preToolUse command hooks fail closed on a crash or non-zero exit; timeouts fail open, and so do most other events
Windsurf, now documented as Devin Desktop (hooks) No blocking hook; post_cascade_response runs asynchronously and can’t block pre_run_command or pre_write_code exits 2 Any other exit code: the action proceeds

Each row comes from that tool’s official docs, last verified on 11 October 2026; none was tested.

Gemini CLI’s docs also note that for unpaid tier and Google One users, Gemini CLI was replaced by Antigravity CLI on 18 June 2026.

Five of the six can send the agent back when it tries to finish; Windsurf can’t. And in Claude Code, Cursor, Gemini CLI and Windsurf, a hook that crashes or exits 1 lets the action through by default. Copilot’s preToolUse command hook fails closed instead. Before you trust an agent hook as a gate, make it fail on purpose once and watch what happens.

Gates the agent can’t edit away: pre-commit, required checks and rulesets

Pre-commit is fast, and one flag skips it

Git runs a pre-commit hook before each commit, and a non-zero exit aborts the commit. Git’s hooks docs also say it “can be bypassed with the --no-verify option”. The pre-commit framework installs the hook with pre-commit install, and lets anyone skip single hooks with the SKIP environment variable.

The pre-commit docs suggest the fix: add pre-commit run --all-files as a CI step. Then a skipped hook on the laptop only moves the failure to the pull request.

Required status checks and rulesets stop the merge

GitHub’s protected branches docs cover the setting that turns CI into a gate: “Require status checks to pass before merging”. Whether that gate holds comes down to the details:

  • A required check passes on a “successful, skipped, or neutral status”. The troubleshooting page adds that a job skipped by a condition reports “Success”. So keep if: conditions off the jobs you require. A workflow skipped by path filtering stays “Pending” and blocks instead.
  • With “Require branches to be up to date before merging” turned on, the checks must have run against the latest base branch.
  • By default, “the restrictions of a branch protection rule do not apply to people with admin permissions.” An agent working with an admin’s credentials gets the same pass. “Do not allow bypassing the above settings” closes it.

Rulesets do the same job with a named bypass list, and when they overlap with branch protection, “the most restrictive version of the rule applies”. Per the rules list, a ruleset can also set the GitHub App that must report a check, so a status with the same name from somewhere else doesn’t count.

Security scanning becomes a gate through the ruleset rule “Require code scanning results”. The merge protection docs let you pick the threshold per tool. Alerts go from None to All, and security alerts from Critical, “High or higher” or “Medium or higher” to All. Per the rules list, the merge is also blocked while a required tool’s analysis is still running, or when the tool isn’t set up.

A cloud agent that opens pull requests meets the same gates

Copilot’s cloud agent opens pull requests on GitHub, so these gates apply to it directly. Its risks and mitigations page says the agent “is also subject to any branch protections and required checks for the working repository”. By default, its workflows don’t run until “a user with write access to the repository clicks the Approve and run workflows button”. The same page says the person who asked for the pull request can’t approve it.

Failures a gate should hand to a person

Some red checks have no answer a tool can pick. The gate should stop and hand the call to a person:

Failure The shortcut that turns it green Who decides
A flaky test Re-run until it passes A person: fix it, or quarantine it with an issue
A changed snapshot Re-record every snapshot A person: is the new output right?
A skipped test Add .skip, or delete the test A person: is the skip justified?
A licence flag Add the licence to the allow list A person, often legal
A security alert Dismiss it as a false positive A person who owns the risk

Snapshots and licences have a tool setting behind them. Jest’s CLI docs say that with --ci, a new snapshot fails the test “Instead of the regular behavior of storing a new snapshot automatically”. The same page says --updateSnapshot re-records “every snapshot that fails during this test run”. That one flag turns every snapshot failure green, which is why it belongs to a person.

For licences, GitHub’s dependency review action has deny-licenses to “Block the pull request on these licenses” and allow-licenses to “Only allow these licenses”. A licence outside the list is a decision for whoever owns that policy. A later part of this series goes deeper into dependencies.

Don’t let the agent weaken a gate to pass

An agent asked to make the checks pass can fix the code, or change the checks. Changing the checks looks like this:

  • a deleted test, or a loosened assertion
  • a new .skip, xit or xfail
  • a lower coverage threshold
  • a new eslint-disable or @ts-ignore
  • continue-on-error: true on a failing step
  • a commit made with --no-verify
  • an edited hook, workflow or check config
  • snapshots re-recorded with -u

Telling the agent not to do this, in the rules file or in Prompt 2 below, helps. It is still a request. To make weakening harder, protect the gates themselves:

  1. Block edits to gate files inside the agent. Claude Code already treats .git, .husky, .claude and .pre-commit-config.yaml as protected paths. In Manual and acceptEdits modes a write there always asks, and allow rules can’t pre-approve it. For other gate files, such as workflows and test config, adapt the hooks guide’s protect-files example. It is a PreToolUse hook on Edit and Write that exits 2. A shell command can still write the file, so code owners and CI are the backstop.
  2. Give gate files code owners. GitHub’s code owners docs say that with “Require review from Code Owners” on, changed files need their owners’ review. The same docs suggest owning the CODEOWNERS file itself.
  3. Read gate changes first in review. Copilot’s review docs say to “be especially alert to any proposed changes in the .github/workflows/ directory”. That advice works for any agent’s pull request.

An example CODEOWNERS file that puts the gates behind a maintainer:

.github/CODEOWNERS
# Example: changes to the gates need a maintainer's approval
/.github/CODEOWNERS       @your-org/maintainers
/.github/workflows/       @your-org/maintainers
/.pre-commit-config.yaml  @your-org/maintainers
/.claude/                 @your-org/maintainers
/jest.config.js           @your-org/maintainers

What the agent can reach in the first place is the subject of Part 6.

Two prompts to copy

Neither prompt relies on a tool-specific feature.

Prompt 1: map the definition of done to gates

An example prompt to use once per repository, to find the items nothing enforces yet.

Prompt 1: map the definition of done to gates
Task: map our definition of done to the checks that enforce it.

Definition of done: <paste your checklist>

Inspect only what this task needs: CI workflows, git hooks and
pre-commit config, agent hook settings, and test, lint, type
check and build config. If you can read branch protection or
ruleset settings with a read-only command, include them.
Otherwise, list what I need to check by hand.

For each item in the definition of done, report:
1. What enforces it today (agent hook, pre-commit, CI check,
   required review), with the file
2. Whether it blocks, warns, or relies on someone remembering
3. How it could be skipped (--no-verify, an admin bypass,
   a skipped job, a check that is not required)
4. Your recommendation: block, warn or a human decision,
   and why

List every item with no enforcement at all.

Do not change any files yet.
Never print, expose or commit secret values.
Do not commit, push, deploy, run destructive commands or make
external changes unless I explicitly authorise it.
If two reasonable attempts fail, stop and report the failure
rather than repeatedly guessing.

Then wait for approval.

Prompt 2: fix the code when a check fails

An example prompt to paste when a check fails and the agent is about to fix it.

Prompt 2: fix the code when a check fails
Task: <describe the task>
Failing check: <paste the command and its output>

Make the failing check pass by fixing the code.

Do not weaken any gate to make it pass:
- no deleting, skipping or loosening tests
- no lowering coverage or other thresholds
- no new lint or type check suppressions
- no changes to CI workflows, hook settings or check config
- no re-recording snapshots
- no --no-verify, and no skipping hooks

If you believe the test or the check itself is wrong, stop and
explain why, with evidence. I will decide.

If the failure looks flaky, or is a licence or security
finding, report it and stop. Those are my decisions.

Never print, expose or commit secret values.
Do not commit, push, deploy, run destructive commands or make
external changes unless I explicitly authorise it.
If two reasonable attempts fail, stop and report the failure
rather than repeatedly guessing.

Report what you changed, the command you re-ran and its result.

A quality gate checklist before you trust a green build

Quality gate checklist
QUALITY GATES: BEFORE YOU TRUST GREEN

□ Every definition-of-done item has a gate or a named person
□ The agent's finish hook exits 2 on failure, not 1
□ The agent hook was made to fail once, and it blocked
□ CI runs the same checks as pre-commit, so --no-verify only delays
□ The checks that must pass are required status checks
□ Required jobs have no condition that can skip them
□ Admins can't bypass, or the bypass list is short and named
□ Code scanning results block at the severity you chose
□ Gate files (workflows, hook settings, test config) have code owners
□ Warnings land where a reviewer reads them
□ Flaky tests, snapshots, skips, licences and alerts go to a person
□ Green CI is read as "the checks passed", not "the requirement is met"

Quick answers

What is a quality gate in software development?

A quality gate is an automated check that blocks a change from moving forward until it passes. Tests, lint or a security scan that must succeed before a pull request can merge are common gates. A check that only reports a result, without blocking, is a warning.

How do you stop an AI coding agent from finishing with failing tests?

A finish hook stops an AI coding agent from finishing with failing tests by running them and sending the agent back. Claude Code calls it a Stop hook and GitHub Copilot an agentStop hook. In Claude Code it must exit with code 2, since exit code 1 does not block.

Can a coding agent skip a pre-commit hook?

A coding agent can skip a pre-commit hook, like any process that runs git commit --no-verify. Running the same checks in continuous integration (CI) as required status checks means a skipped local hook only moves the failure to the pull request.

Does a skipped GitHub Actions job count as a passed check?

A GitHub Actions job skipped by an if: condition reports success, and a required status check accepts a skipped status. A workflow skipped by path filtering is different: its checks stay pending and block the merge.

Does passing CI mean AI-generated code is correct?

Passing continuous integration (CI) shows only that the checks someone wrote have passed. Whether the requirement is met, and whether the tests cover the new behaviour, still needs a person to judge.

More on AI coding is in the AI category.

Cartoon of Kousigan making a heart with his hands

Kousigan A

Build with AI. Ship with care.

Software engineer building real products with AI assistants. I write down what works: prompts, rules, skills and agents, and how to ship safely.