GitHub Branch Protection
Stop accidental pushes to the default branch and require review or checks before history can move.
Protection is GitHub, not Git
Git Branching Workflow wrote a contract: short-lived branches return through pull requests. A contract that nobody enforces is a README. GitHub branch protection (a branch ruleset, or a classic protection rule) is GitHub refusing to move a hosted branch unless the rule is satisfied. It is not a Git command. There is no git protect.
Local main
You can still commit on main. git push origin main is still the Git request to update the remote. Git does not know about GitHub rules.
Hosted main
GitHub can answer that request with a rejection. The pull-request path from GitHub Pull Requests is how main is supposed to move.
# These are Git. None of them is GitHub branch protection.
git switch main
git push origin main
git branch --listUse a disposable GitHub repository you own. Do not edit protection on a project you do not maintain. Do not lock the original main of a fork you just created.
Require a pull request, not a lockout
On GitHub: Settings → Rules → Rulesets (or the older Settings → Branches classic rule). Target the default branch, usually main. The job for this lesson is one switch with a clear meaning.
Require a pull request
Direct pushes to main should fail. This is the GitHub enforcement of GitHub Flow.
Block force pushes
Protected main should not accept git push --force. You already refused force-push after review started; GitHub can refuse it on the default branch too.
Required approvals
Leave this off (zero) on a repository where you are the only reviewer. One required approval with no second person is a closed door.
Required status checks
Leave this off until GitHub Actions has a workflow that has already passed. A required check that does not exist blocks every pull request.
Rulesets and classic branch protection are both GitHub. If the UI offers a ruleset, use it. The proof is the same: git push origin main is rejected, and a pull request still merges.
Prove origin refuses a direct push
After the rule targets main, do not guess. Make a local commit on main and push. GitHub should refuse. Read the error. That refusal is the lesson, not a broken remote.
# On a clone of a GitHub repository YOU own, after protection
# requires a pull request before merging into main.
git switch main
git pull origin main
# A tiny local commit on main is still legal Git.
# GitHub can still refuse to move origin/main.
echo "protection probe" >> PROTECTION.md
git add PROTECTION.md
git commit -m "Probe a direct push to protected main"
git push origin mainTest what you learned
Type the Git command that asks origin to update main from your local main (protection should refuse it).
Local main now has a commit origin/main does not. That is the same “ahead” state Git Remote taught. The difference is GitHub’s policy, not a new Git object.
Recover through a pull request
Do not force-push. Do not turn the rule off to sneak the commit through. Move the probe onto a feature branch, reset local main to origin/main, and open a pull request. That is the path protection is there to keep.
# Leave the probe commit on a feature branch instead of fighting main.
git switch -c feature/protection-probe
git push -u origin feature/protection-probe
git switch main
git reset --hard origin/main
gh pr create --base main --title "Add protection probe note" --body "$(cat <<'EOF'
## Summary
Proves the default branch refuses a direct push and still accepts a pull request.
## How to verify
1. Confirm git push origin main is rejected while this rule is on.
2. Confirm this pull request can still merge.
EOF
)"git reset --hard origin/main on local main is safe here only because the probe commit is already on feature/protection-probe. If you reset first, you can lose the commit. Match the order above. Then merge the pull request the way GitHub Code Review already taught.
Test what you learned
Type the command that creates and switches to a branch named feature/protection-probe.
CODEOWNERS is GitHub, stored in Git
A CODEOWNERS file is an ordinary path in the repository. Git records it. GitHub reads it on the default branch to suggest or require reviewers for matching paths. Put it at .github/CODEOWNERS so a stranger can find it.
# .github/CODEOWNERS on the default branch
# Replace with YOUR GitHub username. This is not a Git command.
* @YOUR_GITHUB_USERNAME
README.md @YOUR_GITHUB_USERNAMEReplace YOUR_GITHUB_USERNAME with the account that owns the practice remote. A made-up handle is a GitHub error, not a Git error. Requiring a review from listed owners is a separate protection checkbox. On a solo repo, add the file and leave that requirement off so you can still merge.
Test what you learned
Type the Git command that stages .github/CODEOWNERS.
Inspect the rule
# From the same clone, after a rule targets main
gh api repos/{owner}/{repo}/branches/main/protection
git ls-remote origin
git branch -vvgh api repos/{owner}/{repo}/branches/main/protection talks to GitHub. If you created a classic rule, you should see JSON that mentions pull requests or reviews. A 404 can mean there is no classic rule—rulesets live at a different API. In that case, confirm the ruleset in Settings and keep the rejected-push proof; do not pretend Git stored the policy.
git ls-remote origin still only lists refs. It will not print protection. That is the Git versus GitHub split again.
What not to enable yet
Required Actions checks
A check that never runs blocks every merge. Add the workflow in the Actions lesson, then require it.
Two reviewers on a solo repo
There is nobody to approve. Zero required approvals plus “require a pull request” is enough proof.
Admin lockout
“Do not allow bypassing” can strand you. Leave owner bypass available on this disposable repo.
Someone else's rules
Do not open Settings on a popular repository. Do not protect upstream from a fork.
Guided practice: protect main
- 01Pick a repository you own
Confirm
git remote -vis your GitHub URL. Update localmain. - 02Add a rule on main
Require a pull request before merging. Block force pushes. Do not require extra approvals or status checks. Do not enforce the rule on administrators.
- 03Prove the rejection
Commit on local
mainand rungit push origin main. Save the error text. - 04Land it through a pull request
Move the commit to
feature/protection-probe, reset localmaintoorigin/main, open and merge the pull request. - 05Add CODEOWNERS
Commit
.github/CODEOWNERSwith your username, through another short-lived branch. Inspect withgh apior the Settings page.
Independent lab: branch protection
- On a GitHub repository you own, add a ruleset or classic rule that requires a pull request before merging into
main. Do not require two reviewers. Do not require a status check. Do not lock administrators out. - Make a local commit on
mainand rungit push origin main. Record that GitHub refused. Do not force-push. Do not turn the rule off to sneak the commit through. - Move the work onto a feature branch, reset local
maintoorigin/main, open a pull request, and merge it. - Add
.github/CODEOWNERSnaming your GitHub username. Showgh api repos/{owner}/{repo}/branches/main/protectionor a screenshot of the ruleset, plus the rejected-push error. - Write six lines: repository URL, which GitHub setting you enabled, the rejected command, how the change still landed, the CODEOWNERS path, and one sentence that distinguishes GitHub protection from
git push. Do not change protection on a project you do not maintain.
Common protection mistakes
Thinking Git enforces the rule
Local commits on main still work. The refusal happens when GitHub receives the push.
Requiring a check that does not exist
Every pull request sits blocked. Wait for Actions.
Requiring reviewers you do not have
A solo practice repo needs a required pull request, not a two-person approval quorum.
Turning the rule off to push
That is not proof. The feature-branch path is the proof.
Lesson review
You can protect main on GitHub so a direct push is refused, keep a pull-request path open, and store CODEOWNERS in Git for GitHub to read. Repeatable issue and pull request shape is GitHub Collaboration. Required status checks are GitHub Actions.
- I know GitHub branch protection is not a Git command and not a Git object.
- I can require a pull request on
mainwithout locking a one-person repo or inventing a status check. - I can prove
git push origin mainis refused, then land the same commit through a pull request. - I can add
.github/CODEOWNERSand inspect the rule withgh apior Settings, on a repository I own.