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
Contents
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:
# 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: trueMake 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:
#!/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 0Then register the example script in .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, orneutralstatus”. The troubleshooting page adds that a job skipped by a condition reports “Success”. So keepif: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,xitorxfail - a lower coverage threshold
- a new
eslint-disableor@ts-ignore continue-on-error: trueon 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:
- Block edits to gate files inside the agent. Claude Code already treats
.git,.husky,.claudeand.pre-commit-config.yamlas protected paths. In Manual andacceptEditsmodes 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 aPreToolUsehook onEditandWritethat exits 2. A shell command can still write the file, so code owners and CI are the backstop. - 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
CODEOWNERSfile itself. - 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:
# 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/maintainersWhat 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.
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.
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 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.

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.