Branch protection rules tell GitHub to refuse any change to main that does not pass the gates you set. The fastest useful setup: go to your repository's Settings, Branches (or Settings, Rules, Rulesets on newer accounts), add a rule that matches main, and turn on Require a pull request before merging plus Do not allow force pushes and Do not allow deletions. With that in place nobody can push straight to main, rewrite its history, or delete it. Everything else is tightening.
This is the safety net that makes a team Git workflow safe to run. If you have not set up a workflow yet, start with GitHub Flow vs Git Flow vs trunk-based development, then come back here to lock the default branch down. New to Git in general? The Git for beginners guide is the place to start. Spinning up a fresh repo? You can set up branch protection from scratch on a new repo as part of the initial setup, before any code lands.
What is a branch protection rule?
A branch protection rule is a set of conditions GitHub enforces server-side on one or more branches. It is enforced at push time and at merge time, so it does not depend on anyone running the right command locally. A developer can configure whatever they want on their machine; the rule still wins when the push reaches GitHub.
Two things matter about that:
- It is server-side. Unlike a pre-commit hook, which a developer can skip with
--no-verify, a branch protection rule cannot be bypassed from a clone. The only way around it is to have the permission to bypass it, which you grant deliberately. - It is per-branch-pattern. You protect
main, orrelease/*, ormainand everyrelease/*at once with a pattern. You do not protect a repository wholesale; you protect the branches that matter.
GitHub has two systems that do this. The older one is called branch protection rules (Settings, Branches). The newer one is rulesets (Settings, Rules, Rulesets), which can layer multiple rules, target tags as well as branches, and apply across many repos in an organization. The individual protections below exist in both; I will use the classic branch-protection names because they are clearer, and note the ruleset equivalent where it differs.
Require a pull request before merging
This is the one rule you should never skip. Turning it on means commits reach main only through a merged pull request, never through a direct git push. Anyone who tries the latter gets rejected:
remote: error: GH006: Protected branch update failed for refs/heads/main.
remote: error: Changes must be made through a pull request.
That rejection is the rule doing its job, and it reads a lot like the non-fast-forward push rejection you hit when your branch is behind the remote: a server-side refusal, not something to override. The fix is not to fight it; it is to do the work on a branch and open a pull request instead. If you are not comfortable with branches yet, branching explained for beginners covers the model this rule assumes.
Under Require a pull request before merging you get sub-options worth setting:
- Require approvals (set the count, usually 1 or 2). The PR cannot merge until that many reviewers approve.
- Dismiss stale pull request approvals when new commits are pushed. If someone pushes more commits after an approval, the approval is dropped and re-review is required. Without this, a reviewer approves a small change and the author pushes something entirely different on top.
- Require review from Code Owners (covered below).
- Require approval of the most recent reviewable push, so the person who pushed the latest commit cannot also be the one approving it.
Require status checks to pass
A status check is a result reported back to the commit by CI: your test suite, a linter, a build, a security scan. Requiring status checks means the PR cannot merge until those checks come back green.
remote: error: GH006: Protected branch update failed for refs/heads/main.
remote: error: Required status check "test" is expected.To use this, your CI has to run on pull requests first so GitHub has seen the check at least once and can offer it in the list. If you run tests through GitHub Actions, each job's name shows up as a selectable required check after its first run on a PR.
Turn on Require branches to be up to date before merging alongside it when you want CI to have run against the latest main. It forces the PR branch to be current before the merge, which catches the case where two PRs each pass on their own but break when combined. The cost is more rebasing or merging on busy repos (the merge vs rebase when updating a PR branch trade-off applies here directly), so enable it where correctness matters more than merge throughput.
Block force-pushes and deletions
Two separate switches, both important, both about protecting history.
Do not allow force pushes blocks git push --force (and --force-with-lease) to the protected branch. A force-push rewrites history: it can erase commits other people have already pulled, which is how a shared branch's history gets quietly destroyed. On main, you almost never want anyone rewriting published history. (Force-pushing your own feature branch is fine and normal; this rule is about the shared branch only.)
Do not allow deletions blocks git push origin --delete main and the delete button in the UI. It stops the entire branch from being removed, by accident or otherwise.
If you ever need to undo a bad commit on a protected branch, you do not force-push over it. You merge a new commit that reverses it, which is exactly the distinction in git reset vs git revert: revert is the history-preserving, protection-friendly way to back something out. And if commits truly went missing, recovering them with git reflog usually beats any rewrite.
CODEOWNERS: require the right people to review
A CODEOWNERS file maps paths in your repo to the people or teams responsible for them. Combined with Require review from Code Owners, it means a PR touching a given path cannot merge until an owner of that path approves, on top of your normal approval count.
The file lives at .github/CODEOWNERS, CODEOWNERS in the repo root, or docs/CODEOWNERS. Each line is a path pattern followed by one or more owners:
# Default owners for everything in the repo
* @acme/platform-team
# Frontend code is owned by the web team
/src/web/ @acme/web-team
# Anyone touching CI config needs a sign-off from these two
/.github/ @alice @bob
# Database migrations need a DBA
*.sql @acme/dba-teamThe patterns work like .gitignore patterns, and the last matching line wins, so order from general to specific. (If you are fuzzy on the matching syntax, how .gitignore patterns work explains the same rules.) When a PR changes a file, GitHub looks up its owner and requests their review automatically; the protection rule then refuses the merge until that owner approves.
Two gotchas I have hit:
- An owner must have write access to the repo, or GitHub silently ignores the line.
- A user cannot be a code owner of files in a PR they authored, for the purposes of satisfying the requirement. Their own approval does not count toward their own code.
Which protections to turn on
Here is how I think about the common ones. Start with the first three on any shared repo; add the rest as the team and the stakes grow.
| Protection | What it stops | Turn on when |
|---|---|---|
| Require a pull request | Direct pushes to main | Always, on any shared branch |
| Do not allow force pushes | History rewrites on the shared branch | Always |
| Do not allow deletions | Branch being deleted | Always |
| Require approvals (1+) | Unreviewed code merging | More than one person commits |
| Require status checks | Merging with red CI | You have CI on PRs |
| Require branches up to date | Merging stale code that breaks on main | Correctness over merge speed |
| Dismiss stale approvals | Approving A, merging B | Reviews need to mean something |
| Require Code Owners review | Wrong person reviewing sensitive paths | You have clear ownership |
| Include administrators | Admins quietly bypassing the rules | You want the rule to mean everyone |
That last row matters more than it looks. By default, repository admins can bypass branch protection. Turning on Do not allow bypassing the above settings (the classic checkbox was "Include administrators") makes the rules apply to admins too, which is usually what you actually want: a rule with a quiet exception for the most privileged accounts is the rule most likely to be the source of an accident.
Setting it up, step by step
Classic branch protection:
- Go to your repository on GitHub.
- Settings, Branches.
- Under Branch protection rules, click Add branch protection rule (or Add rule).
- In Branch name pattern, type
main(or a pattern likerelease/*). - Check Require a pull request before merging, set the approval count, and tick Require review from Code Owners if you have a
CODEOWNERSfile. - Check Require status checks to pass before merging and select your CI checks.
- Scroll down and confirm Allow force pushes is off and Allow deletions is off (these are off by default, but verify).
- Optionally check Do not allow bypassing the above settings.
- Click Create.
On rulesets (Settings, Rules, Rulesets), the flow is the same idea: New ruleset, set Enforcement status to Active, add main as a target under Target branches, then enable the same checks under Rules. Rulesets are the better choice when you need to protect many repos at once or layer more than one rule on a branch.
You can manage the same rules through the GitHub CLI or the REST API for repeatable setup across repos, but the web UI is the right place to learn what each switch does first.
A workflow that fits a protected main
Protection only feels good if your day-to-day flow expects it. The loop is simple:
git switch -c fix-login-bug # branch off main
# ... make your changes, commit ...
git push -u origin fix-login-bug # push the branch, not mainBefore you open the PR, it pays to tidy the branch: squash the commits with an interactive rebase so the history a reviewer reads on a protected main is clean rather than a trail of "wip" and "fix typo", and follow good commit message conventions so each message tells the reviewer what the change does. Then open a pull request, let CI and reviewers do their thing, and merge through the GitHub UI. The first push of a new branch needs the -u flag to set its upstream; if you forget it you will hit the no upstream branch message, which is harmless and tells you exactly what to run. From there, creating the pull request is the only way your code reaches main, which is the whole point.
For teams formalizing this beyond a single repo, the team workflow guide covers how GitHub Flow, Git Flow, and trunk-based development each lean on these same protections.
Sources
Authoritative references this article was fact-checked against.
- About protected branches - GitHub Docsdocs.github.com
- About code owners - GitHub Docsdocs.github.com
- About rulesets - GitHub Docsdocs.github.com





