skip to content

Branch Protection and Rulesets

The rules that make main un-breakable: required checks, required approvals, linear history, signed commits. Rulesets are the newer, layerable, org-level replacement for classic branch protection, and knowing how the two evaluate together is a common senior-level question.

part ofGitHuboverview, primer and where to startread it →
on this pageshow

questions

6

In GitHub, what happens when you push straight to main if a rule requires a pull request?

level: juniorimportance: must knowfreq 68%

answer

  1. the decision happens on the server
  2. your local commits are unaffected
  3. the ref update is rejected
  4. remote error GH006, protected branch
  5. branch it and open a pull request

basics

~20 s

GitHub's server refuses the push. The remote rejects the ref update with a protected-branch error, nothing lands on main, and your local commits are untouched — you push a topic branch and open a pull request instead.

solid answer

~50 s

Branch protection lives on GitHub's side, not in your local Git. When the push reaches the server, GitHub evaluates the rules that target `main`; if a rule requires a pull request before merging, the ref update is rejected and the client prints `! [remote rejected] main -> main (protected branch hook declined)` along with a `remote: error: GH006: Protected branch update failed` line naming the reason. Nothing is lost locally — your commits are still on your local `main`. The normal recovery is to move them onto a topic branch (`git switch -c feature/x`), push that branch, and open a PR, which is the path the rule is forcing you onto. Whether anyone can push directly at all depends on the rule's bypass configuration; by default not even repository admins are exempt unless they are listed as bypass actors.

code

console · 8 lines
console
$ git push origin main
Enumerating objects: 5, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: error: GH006: Protected branch update failed for refs/heads/main.
remote: error: Changes must be made through a pull request.
To github.com:acme/payments-service.git
 ! [remote rejected] main -> main (protected branch hook declined)
error: failed to push some refs to 'github.com:acme/payments-service.git'

go deeper

for a junior

Recall that the rejection comes from GitHub, not your machine, and that nothing local is lost. Be able to describe moving the commits to a new branch and opening a pull request.

for a middle

Explain that GitHub evaluates rules targeting the ref when the push arrives, and that the reason line in the remote error tells you which rule fired — required PR, restricted pushes, signatures, or blocked force pushes.

for a senior

Show how you would unblock a stuck teammate without data loss, and how you would confirm from the repository configuration which rule rejected the push rather than guessing from the message.

for a principal

Own the policy stance: whether any direct-push path to main exists at all, who may bypass, and how a break-glass push is recorded and reviewed afterwards.

## Where the rule actually lives A protected branch is a **server-side** policy on GitHub, not a setting in your working copy. Your local Git happily creates commits on `main`, because your clone has no idea any policy exists. The check happens when `git push` sends the ref update to GitHub: the receiving side looks at every rule targeting `refs/heads/main` — a classic branch protection rule, one or more rulesets, or both — and either accepts or refuses the update. This is why a colleague can commit to `main` locally all day and only discover the policy at push time. ## What the rejection looks like The transport reports a per-ref failure, so the push fails atomically for that ref. The `remote:` lines are GitHub speaking. `GH006: Protected branch update failed for refs/heads/main` is the classic branch-protection code, followed by a human-readable reason such as `Changes must be made through a pull request` or `At least 1 approving review is required by reviewers with write access`. When the rule comes from a ruleset instead, the message names the ruleset and the specific rule that was violated. Either way, the exit status is non-zero, and Git's summary line is `error: failed to push some refs to ...`. ## Nothing is lost A rejected push changes nothing anywhere. The commits still exist in your local repository, reachable from your local `main`; the remote's `main` is byte-for-byte what it was. This matters because the panic reaction — `git push --force`, or `git reset --hard` to "clean up" — is exactly what destroys work. Force is useless here anyway: the rule rejects the update regardless of how it is expressed, and a separate rule normally blocks non-fast-forward pushes outright. ## The intended path Move the work onto a branch and open a pull request: 1. `git switch -c feature/rate-limit` — the branch now points at your commits. 2. `git switch main && git reset --hard origin/main` — put local `main` back in sync with the remote. 3. `git push -u origin feature/rate-limit`, then open the PR. The PR is where the rest of the policy gets applied: required approvals, required status checks, conversation resolution, and whatever else the branch's rules demand. Merging through the PR is a merge performed **by GitHub on the server**, which the rules explicitly permit; a direct push is what they forbid. ## Related rules you may hit instead "Require a pull request before merging" is only one of several rules that reject a push: - **Restrict who can push** (classic) / **restrict updates** (rulesets) — pushes are allowed, but only from listed people, teams or apps. - **Block force pushes** (`non_fast_forward` in rulesets) — normal pushes are fine, history rewrites are not. - **Restrict deletions** — `git push origin :main` is refused. - **Require signed commits** (`required_signatures`) — an unsigned commit in the pushed set is rejected. - **Lock branch** — the branch is read-only entirely, including for merges. Each produces its own reason line, so read the `remote: error:` text rather than guessing. ## Can an admin get through? Only if configured. Classic branch protection has a **"Do not allow bypassing the above settings"** checkbox; leave it on and admins are held to the same rules. Rulesets express the same idea as an explicit **bypass list** of roles, teams and GitHub Apps, optionally scoped to "for pull requests only" (they may open and merge PRs that skip a rule, but still cannot push directly). A repository where everyone believes "admins can always push" almost always has a bypass entry someone forgot about, and the bypass is recorded — rulesets surface it in rule insights and the organization audit log. ## Interview framing The point of the question is whether you understand that policy is enforced by the hosting platform at receive time, not by your Git client, and that a rejected push is a safe, reversible state. Say that, name the error you would read, and describe the branch-plus-PR recovery, and you have answered it fully.

  • Does the rule stop you from committing to main locally?
    No. Your clone knows nothing about GitHub's branch rules, so commits on a local `main` are created normally. Enforcement happens only when the ref update reaches GitHub, which is why the surprise always arrives at push time rather than commit time.
  • You are a repository admin — can you push directly anyway?
    Only if the configuration says so. Classic branch protection has a "Do not allow bypassing the above settings" toggle that holds admins to the rules; a ruleset instead names bypass actors explicitly. If neither grants you an exemption, your push is rejected exactly like anyone else's, and any bypass you do use is recorded.
  • How does the rejection differ when the rule comes from a ruleset rather than classic protection?
    The push is refused the same way, but the message identifies the ruleset and the specific rule that was violated rather than a single generic protected-branch reason. That is more useful for diagnosis, because several rulesets can target the same branch and you learn which one blocked you.

saying these in an interview costs you the question

  • Believes the rejected commits are lost or rolled back
  • Calls branch protection a local Git hook in the clone
  • Reaches for git push --force to get past the rule
  • Assumes repository admins are always exempt
  • Thinks the branch is read-only even for pull-request merges

context

open as a page

In GitHub, what does requiring branches to be up to date add to required status checks?

level: middleimportance: must knowfreq 64%

basics

~20 s

It forces the pull request branch to contain the base branch's newest commit before merging, so the required checks proved green on that exact combination. Without it, checks only prove the branch was green against an older base.

open as a page

In GitHub, what does the Require linear history branch rule actually prevent?

level: middleimportance: should knowfreq 42%

basics

~10 s

It rejects any commit with more than one parent landing on the branch. Practically, the merge-commit button is unavailable on that branch: only squash merge and rebase merge can produce commits GitHub will accept.

open as a page

In GitHub, what can repository rulesets do that classic branch protection cannot?

level: middleimportance: should knowfreq 50%

basics

~20 s

Rulesets layer: several can target one branch and all apply. They can be defined once at organization level across many repositories, target tags and pushes as well as branches, carry an explicit bypass list, run in a non-blocking evaluate mode, and are visible to anyone with read access.

open as a page

If two GitHub rulesets and a branch protection rule target main, which rules apply?

level: seniorimportance: should knowfreq 40%

basics

~20 s

All of them. GitHub aggregates every rule from every applicable ruleset and from classic branch protection, and where they overlap the most restrictive setting wins. There is no first-match precedence and no way for one to relax another.

open as a page

How would you decide who may bypass GitHub branch protection, and keep it auditable?

level: principalimportance: should knowfreq 30%

basics

~20 s

Start from nobody. Grant bypass to roles, teams or apps rather than individuals, scope it to pull requests where possible, keep the emergency path deliberately slow and visible, and review every recorded bypass — rule insights and the audit log — on a fixed cadence.

open as a page