AI coding

Branch Protection Rules for Agent Commits: What to Require

Antonio Evans11 min read

A code review desk with a printed branch protection rule sheet pinned to a wall.
Key points

Key points

  1. 1Require at least one human approval, a passing CI status check, and dismissal of stale approvals so an agent cannot reuse a green result from an older commit.
  2. 2Restrict force pushes and branch deletion, because a rewritten history bypasses review entirely even when reviews are required.
  3. 3Keep the bypass list small and named, since anyone with admin or maintainer rights can merge around the rules unless you include them in the rule.
  4. 4Test the configuration by attempting the bypass yourself: an agent merge that succeeds tells you more than a settings screenshot.

Map the failure modes before you touch a setting

Branch protection is a small number of switches, and each one closes a specific hole. The useful way to design a rule set for agent commits is to start from the holes rather than the switches.

An agent is not a special kind of author as far as the forge is concerned. It is an account, an app installation, or a token with a permission set. GitHub's branch protection rules apply to pushes and merges on matching branches, and by default the restrictions do not apply to people with admin permissions or a role carrying the "bypass branch protections" permission about protected branches. That default is the first failure mode and it has nothing to do with whether the author is a human.

The second failure mode is approval staleness. An agent opens a pull request, a reviewer approves it at a commit that passes tests, then the agent pushes a follow-up commit that weakens an assertion or deletes a test. If the approval is not dismissed on new commits, the merge button stays green. GitHub documents the option to dismiss stale pull request approvals when commits are pushed about protected branches, and it also documents that with that setting, approving reviews are dismissed as stale if the merge base introduces new changes after the review was submitted managing a branch protection rule.

The third failure mode is the direct push. A bot with a bypass goes straight past the pull request and the status checks, which is exactly the scenario people raise when they want automation to bump a version file the bypass status check thread. You may want that for a narrow class of commits. You almost certainly do not want it for an agent that edits application code.

The fourth failure mode is self-review. If the agent's account can approve, or if a single code owner is the only approver and the agent is mapped to that owner team, the review requirement is decorative. Teams hit this in practice, including automated actions that had to use two separate bot accounts so one could approve the other the code owner self review thread. Treat that workaround as a smell: two bots approving each other is not a human review.

Failure modeWhat it looks likeControl that closes it
Admin or bypass roleA maintainer merges an agent pull request with red checksInclude administrators, or use rulesets and audit the bypass list
Stale approvalApproval given at commit A, agent pushes commit BDismiss stale approvals when new commits are pushed
Direct pushBot pushes to the protected branch with no pull requestBypass list limited to a named group, force push and deletion blocked
Self reviewAgent account or its owner team approvesRequire review from someone other than the last pusher, remove bots from approver teams
Ambiguous checkTwo workflows share a job name, required check passes on the wrong oneUnique job names per workflow, required check points at one workflow

Required status checks: pin them to a job name, not a workflow

A required status check is only as good as the name you selected. GitHub warns that job names must be unique across workflows, because using the same job name in multiple workflows can cause ambiguous status check results and block pull requests from being merged about protected branches. The inverse problem is worse: a shared name can let the wrong run satisfy the requirement.

Two rules make the check trustworthy for agent commits.

First, the required check should be the one that runs the tests you actually rely on, at the exact commit being merged. A green run on an earlier commit proves nothing about the current head. This matters more for agents than for humans because an agent will happily push another commit six seconds after the checks go green.

Second, the check should be fail-closed on removed proof. A diff that deletes a test file, drops a table-driven case, or turns an exact assertion into a truthiness check removes evidence, and a remaining green suite cannot prove that the removed check was redundant QA skills guide on deleted and weakened tests. If you want that as a gate, run a diff-aware job that maps each removed test to the flow it protected before the pull request can merge.

The same principle applies to mutation testing, which is the strongest signal you can put behind a required check when generated tests are involved. A suite can be all green with full line coverage and still miss bugs, because coverage measures which lines ran, not whether an assertion caught a defect CircleCI on mutation testing. Mutation testing injects small faults, runs the suite against each one, and reports survivors: a mutation that leaves the tests passing marks a place where an assertion pulled no weight CircleCI on mutation testing. For AI-generated code, coverage and mutation score diverge, and mutation score is the number that tells you whether the tests detect injected faults across Java, JavaScript and Python toolchains Augment Code guide on mutation testing for AI-generated code.

Be careful about turning a whole-repo mutation score into a required check for every agent pull request. Mutation runs are expensive, and teams routinely restrict them to changed files and run the full campaign on a schedule instead the Stryker diff-scoped issue. A practical arrangement is a fast required check that runs the suite plus a diff-scoped mutation run over changed files, and a nightly full run whose score is tracked but not blocking.

A worked boundary example shows why the required check needs a sharp oracle, not just a passing one. Take a shipping function that charges 500 cents below a cart threshold and zero at or above it. A test that asserts shipping is nonnegative at 1,000 and 6,000 cents exercises both branches and still accepts a fee of 1 cent. Change the comparison from < to <= and both sample totals still produce the expected branch outcome, so the mutation survives unless a test asserts the exact value at the boundary itself PracHub on surviving mutants and meaningful assertions. The lesson for an agent gate: require the check, but also require that someone agreed what the assertion is allowed to see.

Required reviews: one human, and a fresh one

The review setting that matters most for agent commits is not the count, it is the freshness. One required human approval is usually enough for an agent change if that approval is bound to the current head commit. Raising the count to two does not help if both approvals predate a weakening commit.

GitHub offers two related options, and they are not the same. Dismiss stale approvals when new commits are pushed invalidates the approval. Require approval of the most recent push requires an approval from someone other than the last person to push, and users who already reviewed can reapprove after the most recent push to meet the requirement the comparison on Stack Overflow. For an agent that pushes repeatedly, enable both. The first stops an old approval from carrying forward; the second stops the pusher's own account from satisfying the gate.

Then make sure the reviewer is not the agent. GitHub documents that it is best to review changes file by file, leaving comments and marking files as viewed before approving or requesting changes reviewing proposed changes in a pull request. That workflow is easier to follow when the reviewer is a person who did not produce the diff. If your CODEOWNERS file maps a directory to a team that includes the agent's account, the review requirement can be satisfied inside the automation.

There is a reviewable design that reduces the whole problem: separate the tests from the code in different pull requests. Submit the test change first, merge it, then submit the implementation without touching the tests, so reviewers validate the requirements in one pass and the implementation in another, and cannot be tempted to loosen a test to fit the code Yegor Bugayenko on code and tests in different pull requests. For agent work this is a strong default. An agent asked to make a failing suite pass has an obvious incentive to edit the suite, and a two-pull-request flow removes that incentive from the reviewed change.

Merge request approvals in GitLab work on the same principle, configured as default rules with a required count greater than zero, optionally combined with code owners so that both the feature group and any oversight group approve GitLab merge request approval rules. Approval policies can enforce that users who authored or committed changes to a merge request cannot approve it, and can prevent pushing or force pushing to the default branch GitLab merge request approval policies. Those two clauses together are the closest GitLab equivalent of the GitHub combination above.

Force pushes, deletion and the bypass list

Force push and deletion restrictions are the cheapest controls to enable and the easiest to forget when a rule is copied between repositories. By default, each GitHub branch protection rule disables force pushes to matching branches and prevents those branches from being deleted, and you can optionally disable those restrictions about protected branches. Leave them on. An agent that rewrites history on a protected branch has effectively bypassed every review that ever happened on it.

The bypass list deserves more attention than it usually gets. On GitHub, actors may only be added to bypass lists when the repository belongs to an organization managing a branch protection rule. That means a personal repository cannot express the exception at all, and an organization repository can express it too broadly.

Three rules keep the list honest:

  1. Name a group, not individuals. A break-glass group with two or three named humans is auditable; a list of service accounts is not.
  2. Separate the break-glass account from the agent account. The account that runs the agent should never appear in the list, because a compromised agent token then inherits the bypass.
  3. Review the list on a schedule and on every incident. The list changes silently when people change roles.

GitLab expresses the same idea through protected branch settings and approval policies that can block pushing or force pushing to the default branch GitLab merge request approval policies. If your GitLab project has a maintainer group that can push directly, you have a bypass list whether or not you called it one.

A short check you can run against the API keeps the audit current:

# List protected branches and their push/merge access levels.
# Requires an admin or maintainer token; response shows which roles may push.
curl --silent --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.example.com/api/v4/projects/$PROJECT_ID/protected_branches" \
  | jq '.[] | {name, push_access_levels, merge_access_levels}'

On the GitHub side, verify that administrators are included in the rule and that the required check names match the workflows that actually run. The settings page is not evidence. The API response is.

A copyable configuration for both forges

The table below is the rule set I would start from for a repository where agents open pull requests on the default branch. Adjust the counts, not the structure.

ControlGitHub settingGitLab settingWhy it matters for agent commits
Human approvalRequire pull request reviews, minimum 1Approval rule with approvals required at least 1Blocks an agent from merging its own change
Fresh approvalDismiss stale approvals, require approval of most recent pushPrevent author or committer from approvingStops an approval given before a weakening commit
Required checkStatus check pinned to a uniquely named jobRequired pipeline status or approval policy tied to scan resultsProves the tested commit is the merged commit
Self reviewReviewer must differ from last pusherAuthor and committer cannot approveRemoves the two-bot approval pattern
HistoryRestrict force pushes, restrict deletionsProtected branch, no force pushPrevents silent rewrite of reviewed history
BypassNamed break-glass group only, administrators includedMaintainer access limited to a small groupShrinks the set of accounts that can ignore the rule

If you want a single decision rule: require one human approval on the current head, one required check that runs the suite at that head, stale approval dismissal, and no force push. Everything else is tuning. If any of those four is missing, the rest of the configuration is unlikely to stop an agent that is trying to merge.

For the mutation check specifically, a gate that fails closed looks like this in a CI step, scoped to changed files so the run stays short:

BASE=$(git merge-base origin/main HEAD)
CHANGED=$(git diff --name-only "$BASE" HEAD -- '*.ts' ':(exclude)*.spec.ts' | paste -sd, -)

if [ -z "$CHANGED" ]; then
  echo "No source files changed; mutation gate skipped."
  exit 0
fi

npx stryker run --mutate "$CHANGED"

The downside, as the maintainers of the diff-scoped approach note, is that path globs from the Stryker config have to be replicated in the command the Stryker diff-scoped issue. Keep the glob list in one script so the duplication is in a single place.

Where agents make the rules harder than humans do

An agent will produce more commits per hour than a person, and each commit invalidates approvals when stale dismissal is on. That is the point, but it changes the review rhythm. Reviewers learn to stop approving early drafts and wait for the agent to signal done. If you have a pull request template, put the signal in it: a label, a comment, or a draft state that clears when the agent finishes.

Agents also tend to touch more files per change. A diff that spans thirty files is harder to review file by file, which is the workflow GitHub recommends reviewing proposed changes in a pull request. A useful guard is a size check as a required status, alongside the test suite, so an oversized agent change fails before a reviewer spends an hour on it.

Finally, agents regenerate tests. That is fine when the regenerated test still proves the same flow, and dangerous when it silently narrows the assertion. The distinction between retesting and regression testing is worth making explicit in your review checklist: retesting asks whether the fix works, regression testing asks whether the fix broke something else, and both are needed after a code change Drizz on regression testing. An agent asked to fix a failing test will do the former and may skip the latter.

If you are running several agents against several repositories, wiring them through one inference endpoint keeps the credentials and the audit trail in one place. Quanta Inference serves text, image, video, audio and embedding models through a single OpenAI-compatible API key, prepaid and priced in US dollars, which is one fewer set of tokens to rotate when you audit which account touched a protected branch: https://quantaintelligence.ai/inference.

FAQ

FAQ

Do branch protection rules apply to bot accounts and GitHub Apps?

Yes, in the sense that the rules apply to the branch and the merge path, not to the identity of the author. The exception is the default on GitHub that restrictions do not apply to people with admin permissions or a role with the "bypass branch protections" permission, which you can override by including administrators in the rule about protected branches.

Is dismissing stale approvals enough on its own?

No. It closes the gap where an approval given at one commit survives a later, weaker commit managing a branch protection rule, but it does nothing about a bot that can push directly to the branch or an admin who merges with red checks. Pair it with a required check, force push restrictions, and an audited bypass list.

Should I require two approvals instead of one?

Only if a second reviewer adds real coverage. Two approvals given before the agent pushes again are both stale, so freshness matters more than count. Add the require approval of most recent push setting so the last pusher cannot satisfy the gate the comparison on Stack Overflow.

Why require mutation testing rather than line coverage in the status check?

Coverage measures which lines ran, not whether an assertion caught a defect, so a suite can be fully covered and still miss bugs CircleCI on mutation testing. Mutation testing names the surviving mutants and points at the line that produced each one, which is the signal a reviewer can act on when the tests were generated rather than written.

How do I verify the rule set actually holds?

Attempt the bypass yourself. Push a commit that deletes a test with the agent's credentials, confirm the required check fails, confirm the approval is dismissed, and confirm the merge button is blocked. Then have a break-glass account test the exception path and record it. A settings screenshot proves intent; a failed bypass attempt proves the rule.

Sources

Sources

  1. About protected branches
  2. Managing a branch protection rule
  3. Can we have "Allow specified actors to bypass required ...
  4. Allow code owners to review their own PRs #14866
  5. Deleted Tests Weakened Assertions: QA Risk
  6. What is mutation testing? - CircleCI
  7. Mutation Testing for AI-Generated Code: A Practical Guide
  8. Generate mutant only for the changed lines in a range of ...
  9. Mutation Testing Interview Questions: Surviving Mutants and Meaningful ...
  10. GitHub: What is the difference having these two groups of ...
  11. Reviewing proposed changes in a pull request
  12. The Code and Its Tests in Different Pull Requests
  13. Merge request approval rules
  14. Merge request approval policies
  15. Regression Testing: How Teams Prevent Old Bugs from ...
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles