Protected Branches
A protected branch is a branch with extra rules attached on the server side that prevent certain operations — like force-pushing, deleting, or merging without a review. Protections live on the hosting platform (GitHub, GitLab, Bitbucket, Gerrit), not in Git itself, because Git on your laptop has no concept of "the team's policy." They turn an honour-system workflow into an enforced one.
What protection rules actually do
Block force-push —
git push --forceto a protected branch is rejected by the server, so nobody can rewrite shared history by accident.Block deletion —
git push origin --delete mainfails; the branch cannot vanish.Require pull requests — direct pushes are rejected; all changes must go through a PR/MR.
Require N reviewers — the PR cannot merge until N approvals are recorded.
Require status checks — CI must be green; if tests fail, merge is blocked.
Require signed commits — every commit on the branch must be GPG/SSH-signed and verified.
Require linear history — merge commits (or non-fast-forward merges) are rejected; you must rebase or squash.
Require conversation resolution — every review comment thread must be marked resolved before merge.
A protected branch in your push output
What rejection looks like
git push --force origin main # To github.com:acme/payments.git # ! [remote rejected] main -> main (protected branch hook declined) # error: failed to push some refs to 'github.com:acme/payments.git'
Enabling protection on GitHub
On GitHub, open the repo and go to Settings → Branches → Branch protection rules → Add rule. Set a branch name pattern (e.g. main or release/*), then check the boxes you want. Save. From that moment, anyone pushing to that branch is held to the rules.
Typical GitHub protection setup for main
Branch name pattern: main
[x] Require a pull request before merging
[x] Require approvals: 1
[x] Dismiss stale pull request approvals when new commits are pushed
[x] Require review from Code Owners
[x] Require status checks to pass before merging
[x] Require branches to be up to date before merging
Required checks: ci/build, ci/test, lint
[x] Require conversation resolution before merging
[x] Require signed commits
[x] Require linear history
[x] Do not allow bypassing the above settings
[x] Restrict who can push to matching branches
[ ] Allow force pushes
[ ] Allow deletionsThe same idea on GitLab and Bitbucket
GitLab — Settings → Repository → Protected branches. Pick the branch, choose "Allowed to push" and "Allowed to merge" roles. Combine with Merge Request approval rules for review requirements.
Bitbucket Cloud — Repository settings → Branch restrictions. Add restrictions for "Prevent all changes," "Prevent deletion," or "Require merge checks."
Bitbucket Data Center — Branch permissions live under Project or Repository settings, with finer-grained per-user rules.
Azure DevOps — Repos → Branches → Branch policies. Same vocabulary: reviewers, builds, linked work items.
A mental model
Where the policy lives
+--------------------+ +----------------------+
| your laptop (Git) | -----> | hosting platform |
| no rules, no judge | push | pre-receive hook |
| | | checks protection |
+--------------------+ | ACCEPT or REJECT |
+----------------------+Bypass rules and emergency overrides
Sometimes you have a genuine emergency — production is down and the fix has to land now, review be damned. Most platforms let admins bypass protection, but the safer pattern is to leave bypass off and instead temporarily relax the rule, then turn it back on. GitHub has an explicit "Allow specified actors to bypass required pull requests" option; use it sparingly.
Emergency push (admin) — leaves an audit trail
# Only works if your role has bypass permission git push origin hotfix:main # Hosting platform logs: bypass used by alice@acme.com
Recommended rules by branch type
Branch | Reviews | CI required | Linear history | Force push |
|---|---|---|---|---|
main / master | 1–2 + Code Owners | Yes | Yes | No |
release/* | 2 + release lead | Yes (full suite) | Yes | No |
develop (Gitflow) | 1 | Yes | Optional | No |
feature/* | 0 (PR-only review) | No | No | Yes (your own) |
gh-pages | 0 | No | No | Yes |
Self-hosted equivalents
Gerrit — every change is reviewed via Change-Ids; the server refuses pushes that don't come through Gerrit's review workflow. Protection is the default, not the exception.
Plain Git server — write a server-side
pre-receiveorupdatehook that inspects the ref being pushed and rejects writes to protected refs unless conditions are met.Gitea / Forgejo / Codeberg — same UI concept as GitHub; Settings → Branches → Protected Branches.
Minimal pre-receive hook to protect main
#!/bin/sh
# hooks/pre-receive on a bare repo
while read oldrev newrev refname; do
if [ "$refname" = "refs/heads/main" ]; then
if [ "$oldrev" != "0000000000000000000000000000000000000000" ]; then
# Reject non-fast-forward
if ! git merge-base --is-ancestor "$oldrev" "$newrev"; then
echo "Force pushes to main are not allowed" >&2
exit 1
fi
fi
fi
doneCommon errors you'll see
protected branch hook declined— the branch is protected and your push violates a rule.GH006: Required status check is expected— CI hasn't run or hasn't passed yet. Wait, or fix the failure.At least N approving review is required— your PR doesn't have enough approvals.Changes must be made through a pull request— you tried to push directly to a branch that requires PRs.Required signature missing— signed commits are required and your commit isn't signed. See the SSH/GPG signing tutorial.
Real-world workflow with protections
Day-to-day flow when main is protected
# 1. Branch from main git switch -c feat/checkout-button main # 2. Work, commit, push your branch git add . git commit -m "Add checkout button" git push -u origin feat/checkout-button # 3. Open a PR on the platform UI (or with gh) gh pr create --fill --base main # 4. CI runs. Reviewer leaves comments. You push fixes: git commit -am "Address review" git push # 5. Once approved and green, merge via the UI (the only path # that is allowed). Local main updates on next pull. git switch main git pull