The day a second person or an AI agent gets write access to your repository, main needs a rule. At least 17 of the 21 third-party apps I audited in June and July 2026 had no deploy gate: every push ships straight to production with nothing checking it first. GitHub branch protection is that rule, and a PR template makes each change reviewable.

What these controls are, together: GitHub branch protection, a PR template and a contributing guide

GitHub branch protection is a rule on a branch, usually main, that can refuse any change that did not arrive through a pull request with passing required checks. With a PR template and a one-page contributing guide, it makes three things true of every change: it was described, it was checked, and someone allowed to merge it did.

Those 21 were third-party apps: 11 public projects audited exhaustively across all 12 pillars, and a held-out set of 10 others audited blind. They are a selected set, not a random sample, so the opener’s number is no rate for AI-built apps in general. The rest of the release path, from environments to rollback, is laid out in DevOps for startups: the release path.

ControlWhat it isWhy it mattersThe file or setting
Protected branchMain accepts changes only through a pull request with the checks and merge rights the rule setsA push can no longer become a release on its ownSettings, then Branches for a branch protection rule, or Settings, then Rulesets for a ruleset
PR templateA file that pre-fills every pull request with the change, the verification and the release considerationsThe reviewer sees what changed, how it was tested and what the deploy needs.github/pull_request_template.md
Contributing guideOne page that gets a new contributor from a clean checkout to a passing pull requestThe next developer can set up, check and ship without a call with youCONTRIBUTING.md

In GitHub’s own terms, branch protection rules apply to one branch, to all branches, or to any branch matching a name pattern, and they turn on requirements such as passing status checks or a linear commit history. That is how you protect a branch on GitHub: you describe the branch, then tick what it demands. GitHub’s page on protected branches lists every setting.

GitHub has two mechanisms for this: classic branch protection rules and GitHub’s rulesets. Only one branch protection rule applies to a branch at a time, while several rulesets can apply together, and anyone with read access can view a repository’s active rulesets. When a ruleset and a classic rule target the same branch, all the rules are aggregated and the most restrictive version of each one applies. My working rule for one small repository: either mechanism works, so pick one, and read how they layer before you put both on the same branch.

Your plan decides whether any of this exists on a private repository. GitHub’s protected branches page says: “Protected branches are available in public repositories with GitHub Free and GitHub Free for organizations. Protected branches are also available in public and private repositories with GitHub Pro, GitHub Team, GitHub Enterprise Cloud, and GitHub Enterprise Server.” The rulesets page says: “Rulesets are available in public repositories with GitHub Free and GitHub Free for organizations, and in public and private repositories with GitHub Pro, GitHub Team, and GitHub Enterprise Cloud.” GitHub’s plans page lists protected branches and code owners among the paid plans’ tools for private repositories (all three pages read on 2026-09-28). My reading of the three: on a private repository under GitHub Free, the rule, rulesets and code owners on this page are not available, while the PR template and the contributing guide are.

The Production Hardening Sprint’s published scope gives its reasons for these controls in two lines: unchecked changes can otherwise bypass the release process (deliverable 7.2), and consistent review information makes future development easier to assess (deliverable 7.8).

What goes wrong without them

What you seeThe missing controlWhere the fix is
Production changed and nobody can name the changeNo rule on main, so pushes skip the pull requestThe starter rule
CI is red and the merge button still worksNo required status checksThe pull request checks checklist
An agent’s commit lands on mainWrite access with nothing enforced on the serverThe starter rule and the permissions checklist
A pull request with a one-word title and no descriptionNo PR templateThe PR template

Someone pushed straight to main

Production changed, and nobody can say which change did it, because there was no pull request, no check run on a branch and no second pair of eyes. On a host that deploys main on every push, the push was the release. That is the pattern behind the opener’s number, in my reading, and all 5 of my own production apps had no deploy gate either. Those five went through the same audit as the third-party apps, and they are a selected set too, not a sample of AI-built apps.

Picture a founder whose app deploys from main on every push, with a contractor and a coding agent both holding write access and no rule on the branch. A change pushed straight to main breaks a page customers use; CI ran and went red, but nothing required it, and nobody can point to a pull request that says what changed. The lesson I take from it: the repository allowed it, and a rule on main is what turns a red check into a stop.

Undoing the release is a separate question: what a rollback plan is. The git side of undoing the push is in the questions at the end.

The checks run, and nothing requires them

CI exists, it goes red, and the merge button still works. GitHub’s status checks page puts the condition plainly: “If status checks are required for a protected branch, they must pass before the pull request can be merged.” I read the other side of that sentence as the problem: a check nobody marked required is advice, and a pull request can merge while it fails. In the same audits, several apps disable their own type and lint checks at build time, so broken code compiles and ships. Setting up the checks themselves is covered in CI/CD best practices; this page only says which ones to mark required.

The AI agent commits to main

A coding agent with write access, whether Cursor, Claude Code or a builder’s GitHub sync, pushes wherever the repository lets it. In my reading, branch protection is the guardrail an agent cannot talk its way past, because GitHub enforces it on the server when the push arrives, not in the prompt.

An agent can misread a prompt. It cannot misread a branch rule, because the rule is checked on GitHub’s side of the push.

When a protected branch refuses Lovable’s sync, what happens when Lovable’s GitHub sync is refused has the details. The rules file, protected paths and the CI check for agents are covered in guardrails for AI coding agents.

Every pull request is an empty box

When the title is a single word and the description box is empty, the reviewer is left guessing what changed, how it was tested, and whether a migration or a new environment variable rides along. The reviewer then either reads the diff cold or approves it on trust. What a reviewer looks at is in what happens in a code review, step by step, and what a founder who cannot read code gets from one is in code review for non-developers. The template is what makes the review possible; it does not replace it.

How to set them up

Settings first, then people, then the two files. Every setting label below is spelled the way GitHub’s docs print it on 2026-09-28. The steps follow GitHub’s, Atlassian’s and GitLab’s documentation, not a test I ran; the reasons for each choice are mine.

How to protect the main branch: the starter rule, and how to require reviews before merging to main

A starter branch protection rule for a small team, by my working rule, turns on six settings beyond the defaults: a pull request first, one approval, stale approvals dismissed, required status checks, resolved conversations, and no admin bypass. A one-person repository sets approvals to zero, since authors cannot approve their own pull requests. Private repositories need a paid GitHub plan.

The click path: open the repository, click Settings, then Branches in the “Code, planning, and automation” section of the sidebar, then Add classic branch protection rule, and type main under Branch name pattern. GitHub’s own steps are on managing a branch protection rule. To require reviews before merging to main, tick Require approvals under Require a pull request before merging and pick the number. Here is the rule I’d start a team of about one to five people on, setting by setting.

  1. 01 Require a pull request before merging: on. Every change to main arrives as a pull request, so it has a record and a place for checks to run.
  2. 02 Require approvals: on, with one required approval once two or more people work in the repository. The solo case is below the list.
  3. 03 Dismiss stale pull request approvals when new commits are pushed: on. An approval covers the diff the reviewer saw, and GitHub dismisses it when that diff changes.
  4. 04 Require status checks to pass before merging: on, with the checks from the pull request checks checklist below selected by name in the search field.
  5. 05 Require conversation resolution before merging: on. Every review comment is resolved before the merge, so nothing raised in review is left open.
  6. 06 Allow force pushes and Allow deletions: leave both off. On a protected branch GitHub blocks force pushes and deletion by default.
  7. 07 Do not allow bypassing the above settings: on. Without it, the rule does not apply to people with admin permissions, and in my reading that includes the owner of a personal repository.
  8. 08 Require branches to be up to date before merging: off for a small team, unless main breaks often. It makes merges wait on each other.

Two notes on that list. GitHub’s troubleshooting page for required checks says a required status check “must have completed successfully in the chosen repository during the past seven days”, so let the workflow run on one pull request before you pick the checks. And GitHub’s docs call the up-to-date version “the default behavior for required status checks”, so check that box’s state on purpose rather than accepting it.

Left off on purpose, as my calls: Require signed commits (every contributor and bot must sign and verify commits first), Require linear history (merges then have to be squash or rebase), Require merge queue (particularly useful on branches with a relatively high number of pull requests merging each day from many different users), and Lock branch (it makes the branch read-only).

The solo case needs its own answer. GitHub’s page on approving pull requests says: “Pull request authors cannot approve their own pull requests.” With one required approval and nobody else in the repository, every merge waits forever, or someone switches the bypass back on and the rule stops meaning anything. My working rule for a one-person repository: required approvals at zero, and everything else kept. The pull request, the required checks and the no-bypass setting still give you a record and a gate.

A migration file that skips review is the costliest miss in my reading; what runs on deploy is a separate question: how to run database migrations on deploy. If you manage GitHub with Terraform, the integrations/github provider has a github_branch_protection resource that protects a branch in your organization. On a private repository, every setting here needs one of the paid plans quoted in the first section.

Repository permissions checklist: who can merge, and who can bypass

A repository permissions checklist answers two questions: who can merge to main, and who can skip the rule. List everyone with write access or above, every token, deploy key and app that can push, and the bypass list on the rule. For a small team my working rule is an empty standing bypass list.

GitHub’s roles for organization repositories run from least to most access. The table shows the three rights that matter for main, from GitHub’s repository roles page.

RolePush to the repositoryMerge a pull requestManage branch protection rules and rulesets
ReadNoNoNo
TriageNoNoNo
WriteYesYesNo
MaintainYesYesNo
AdminYesYesYes

The same page gives only Admin the right to merge pull requests on protected branches “even if there are no approving reviews”; the no-bypass setting in the starter rule is what takes that right away. Those roles belong to organization repositories. A repository owned by a personal account “has two permission levels: the repository owner and collaborators”, and in a private one “Collaborators can’t have read-only access to repositories owned by a personal account.” Collaborators there can push, so on a personal private repository every collaborator can write.

  • Everyone with write access or above is listed, and anyone who has left is removed.

  • Nobody works as an admin day to day; the admin role is for changing settings.

  • The bypass list on the rule or ruleset is empty, or names a single break-glass role.

  • Deploy keys are read-only unless a job needs to push.

  • Personal access tokens and GitHub Apps with write access are listed with their owner, and an AI builder’s sync app counts as one of them.

  • The default branch is the branch the host deploys.

  • Outside collaborators are reviewed when a contract ends.

On deploy keys, GitHub’s roles page warns that anyone holding a deploy key’s private key “can read from or write to the repository (depending on the key settings), even if they’re later removed from the organization.”

The bypass list is where a rule’s exceptions live. On a ruleset, GitHub lets you add repository admins, organization owners and enterprise owners, the maintain or write role, teams, GitHub Apps and Dependabot. On a classic rule, “Actors may only be added to bypass lists when the repository belongs to an organization”, so on a personal repository that list is empty by necessity.

GitHub bypass requests are a different feature: they come with delegated bypass, which GitHub documents for push protection of secrets and, in its Enterprise Cloud docs, for push rulesets. Where delegated bypass for push protection is on, chosen people can push commits that push protection first blocked, requests from everyone else go through a review cycle, and requests expire after 7 days. GitHub says the feature “is available for the following repository types: Organization-owned repositories on GitHub Team with GitHub Secret Protection enabled”. Secret scanning itself is covered in secret scanning and push protection. Who owns the GitHub organization, and getting it back from a past contractor, is covered in when a developer owns your hosting account.

CODEOWNERS on GitHub, Bitbucket code owners and GitLab protected branches

Code owners are a file, CODEOWNERS, that maps paths to the people responsible for them, who are asked to review changes. On GitHub it blocks a merge only when the branch rule requires review from code owners. GitHub, GitLab and Bitbucket each have a version, and its location, syntax and plan differ by host, so the host’s docs decide.

Code owners are not branch protection. On GitHub, code owners “are automatically requested for review when someone opens a pull request that modifies code that they own”; the merge waits for them only once you tick Require review from Code Owners on the rule, and then an approval from any one of a path’s owners is enough. In GitHub’s words, “You can define code owners in public repositories with GitHub Free and GitHub Free for organizations, and in public and private repositories with GitHub Pro, GitHub Team, GitHub Enterprise Cloud, and GitHub Enterprise Server.” That sentence, like every plan and tier in this section, is as the hosts’ docs printed it on 2026-09-28. GitHub’s CODEOWNERS docs have the full syntax. In a one-person repository, leave Require review from Code Owners off: you would be the only owner, and you cannot approve your own pull request.

The paths worth an owner in a small product, on my list: migrations, auth and billing code, the CI workflow folder, and the CODEOWNERS file itself. GitHub’s docs make the same point about the last one: to protect a repository fully, the CODEOWNERS file needs an owner too. Here is an example in GitHub’s syntax; the paths and the team names are placeholders for yours, and each team needs explicit write access to the repository.

# .github/CODEOWNERS (GitHub syntax). The last matching pattern wins.
/supabase/migrations/  @your-org/backend
/src/lib/auth/         @your-org/backend
/src/lib/billing/      @your-org/backend
/.github/              @your-org/maintainers

Bitbucket CODEOWNERS lives in a .bitbucket folder: Bitbucket Cloud reads .bitbucket/CODEOWNERS, uses a .gitignore-like syntax, and adds code owners to newly created pull requests as suggested reviewers. Bitbucket Cloud code owners names no plan requirement. Merges are held by merge checks under Branch restrictions, and Bitbucket’s merge checks page is direct about the plan: without Premium “we’ll warn users when they have unresolved merge checks, but they’ll still be able to merge”, and preventing the merge means upgrading to Premium and selecting Prevent a merge with unresolved merge checks. On Bitbucket Data Center the file also sits under .bitbucket, and code owners are added to the pull request as reviewers automatically. Marketplace add-ons such as DevSensei’s and Mibex’s code owners apps also exist; I don’t cover them here.

On GitLab, GitLab protected branches come on every tier, and the default branch is protected by default. GitLab Code Owners, code owner approval on a protected branch and merge request approval rules need Premium or Ultimate.

ControlGitHubBitbucket CloudGitLab
File location.github/, the root or docs/, first found in that order.bitbucket/CODEOWNERS./CODEOWNERS, ./docs/CODEOWNERS or ./.gitlab/CODEOWNERS, first found in that order
Syntaxgitignore-style patterns, then @username, @org/team-name or, in most cases, an email address; the last match wins.gitignore-like patterns, then emails, usernames, groups or teams; the last match winsa path pattern, then usernames, groups, roles or emails; later rules win
What owners get on a new pull requestautomatically requested for reviewadded as suggested reviewerslisted in the merge request widget; approval needs the protected branch setting
What holds the mergeRequire review from Code Owners on the branch rulea code owners merge check: not stated in Bitbucket’s docs; merge checks block only on PremiumRequire approval from code owners on a protected branch
Plan for code ownersprivate repositories: Pro, Team, Enterprise Cloud or Enterprise Servernot stated in Bitbucket’s docsPremium or Ultimate
Where branch rules liveSettings, then BranchesRepository settings, then Branch restrictionsSettings, then Repository, then Branch rules

Pull request checks checklist: what must be green before a merge

A pull request checks checklist for a small product starts, on my working rule, with 4 required checks: lint, typecheck, build and tests. Scanning and preview deployments start as advisory. A required check has to be fast and reliable, because a flaky one teaches the team to ask for a bypass.

CheckWhat it catchesRequired or advisory (my rule)
LintPatterns the team has banned, unused code, obvious mistakesRequired
TypecheckA call that no longer matches its types, a renamed field still in useRequired
BuildCode that will not compile or bundle for productionRequired
Tests, including smoke tests of sign-up, login and payment where they existA main flow that brokeRequired
Dependency and secret scanningKnown-vulnerable packages, committed keysAdvisory at first
Preview deploymentA change that looks wrong in a browserAdvisory at first

Scanning is covered in CI/CD security best practices, and previews are a separate topic: what preview deployments are. Name the checks exactly as the workflow’s jobs print them, because the rule selects them by name: for a workflow the format is <job name>, and for a reusable workflow it is <job name> / <reusable job name>. GitHub also says to keep job names unique across all workflows, since the same job name in two workflows can give ambiguous results and block pull requests.

Two traps sit in GitHub’s docs. A job that is skipped “will report its status as “Success”. It will not prevent a pull request from merging, even if it is a required check.” A whole workflow skipped by path filtering, branch filtering or a commit message does the opposite: its checks stay “Pending” and block merging, and GitHub’s advice is to avoid requiring workflows that can be skipped. Building the pipeline itself is the job of the CI/CD page linked above.

The PR template: change, verification, release

A PR template is a markdown file in the repository that pre-fills every pull request. Three headings are enough: what changed and why, how it was verified, and what the release needs, such as a migration, a new environment variable or a rollback note.

GitHub reads it from .github/pull_request_template.md, from pull_request_template.md in the root, or from docs/pull_request_template.md; a .github/PULL_REQUEST_TEMPLATE/ folder holds several, picked with a template query parameter. It appears on new pull requests only once it is merged into the default branch. GitHub’s pull request template docs have the steps. Paste this as a start:

## What changed and why
<!-- A sentence or two. Link the issue if there is one. -->

## How it was verified
<!-- Commands run, screens clicked, and what was NOT tested. -->

## Release considerations
<!-- Migration? New environment variable? Feature flag? Steps after deploy? How to roll back? -->

- [ ] Lint, typecheck, build and tests pass locally
- [ ] Migration reviewed, or none in this change
- [ ] New environment variables added on the host, or none
- [ ] Rollback step written above

Keep it about this short. My reading: a template nobody fills in is worse than none, because the untouched headings make an unreviewed pull request look reviewed.

What goes in a contributing guide, from clean checkout to commit message format

A contributing guide for a private product is one page with 7 headings: prerequisites, setup from a clean checkout, running the checks locally, branch naming, commit message format, how a pull request is reviewed and merged, and how a release happens. It is tested by following it on a clean machine.

GitHub looks for CONTRIBUTING.md in the .github folder, the root or docs, in that order, and shows a link to it when someone opens a pull request or creates an issue, plus a Contributing tab in the repository overview and a Contributing link in the sidebar. GitHub’s contributor guidelines docs cover the rest. Write it for the next developer on a private product, not for strangers on an open-source project: no code of conduct section, no contributor agreement, no issue triage policy.

  • Prerequisites and versions: runtime and package manager versions, and the accounts you need access to.
  • Setup from a clean checkout: clone, install, copy .env.example to .env, set up the database, run the app.
  • Running the checks locally: the same lint, typecheck, build and test commands CI runs.
  • Branch naming: one pattern, such as feature/short-name or fix/short-name.
  • Commit message format: one convention, in two lines.
  • How a pull request gets reviewed and merged: who approves, squash or merge, what the template asks for.
  • How a release happens: what deploys main, who to ask, how to roll back.

For the commit message format, pick one convention and write it in two lines. One published convention, Conventional Commits 1.0.0, uses the shape <type>[optional scope]: <description>, with an optional body and footers. My rule on top of any convention: the summary line says what changed, in plain words. The setup heading is also where the guide says whether to clone over HTTPS or SSH; what the transport protects, and what it leaves open, is a question of man-in-the-middle prevention.

How to verify each one

Branch protection is verified with 7 tests: a direct push to main is refused, a pull request with a failing check cannot merge, an unapproved one cannot merge, an administrator is blocked too, force pushes and deletions fail, the template appears on a new pull request, and the guide works from a clean checkout.

Run them in order after the rule is saved; the second one, the test that failing checks block a merge, is the one the rule exists for. The expected results below are what GitHub’s docs describe, not a record of a run of mine.

  1. 01 Push a commit straight to main from an account with write access. Your own account works once Do not allow bypassing the above settings is on; otherwise ask a colleague to push from theirs. Pass: the remote refuses the push.
  2. 02 Open a pull request with a deliberately failing test. Pass: the merge is blocked while the required check is failing.
  3. 03 Open a green pull request with no approval, in a repository that requires one. Pass: the merge is blocked until someone else approves.
  4. 04 Repeat the failing-test pull request signed in as an administrator. Pass: it is still blocked, which shows the no-bypass setting holds.
  5. 05 Try a force push to main, then try to delete main. Pass: both are refused.
  6. 06 Open a new pull request. Pass: the three template headings appear in the description box.
  7. 07 On a machine or container that has never seen the project, follow the contributing guide word for word to a running app and passing local checks. Pass: no step needed a guess, and every guess you did make becomes a fix to the guide.

For the first test, never make a second account for yourself: GitHub’s terms say “One person or legal entity may maintain no more than one free Account”. When required status checks have not passed, GitHub’s docs say a push to a protected branch returns an error similar to remote: error: GH006: Protected branch update failed for refs/heads/main. Evidence to keep: the refusal text, the pull request and check-run URLs, the rule’s settings copied as text, and the date. Close the test pull requests without merging.

On the sprint’s own deliverables, we verify 7.2 by attempting a failing merge and confirming the protection prevents it, and deliverable 7.8 by creating a pull request using the template and following the guide on a clean checkout.

Where the sprint does this

In the Production Hardening Sprint, deliverable 7.2 protects the main branch with required checks and controlled merge permissions, and deliverable 7.8 provides a short contributing guide and a template covering the change, verification, and release considerations. Their verify steps are the ones under How to verify each one. The result for both goes into the production readiness report, which accounts for all 123 IDs, keeps failures visible until resolved and explains genuine non-applicable items. Hosting, paid tools and API usage are paid through your accounts, and we explain any required costs before enabling them. Both sit with the other release-path deliverables in the release-path checks in the published scope.

Common questions about protecting a main branch

Why can’t I delete this protected branch?

GitHub blocks it by default: “By default, you cannot delete a protected branch.” To delete it, someone with admin permissions either ticks Allow deletions on the rule, deletes the rule, or changes its branch name pattern so it no longer matches. A locked branch cannot be deleted even by someone with permission to delete it.

Are main and master branch the same?

Both are names for a repository’s default branch; what differs is the name. GitHub’s docs say “By default, GitHub names the default branch main in any new repository”, and you can change the default branch of an existing repository. A protection rule matches branch names, so check which name your repository actually uses before you type the branch name pattern.

How do I revert a push to main?

Run git revert on the bad commit from a new branch and send the result through a pull request like any other change. Git’s docs say it “is used to record some new commits to reverse the effect of some earlier commits”, so the history other people already pulled stays as it was. My rule: never undo a shared branch with a reset and a force push; with the starter rule on, main refuses force pushes anyway. Rolling back the deploy itself is a separate job from reverting the commit.

How to keep a branch up to date with main?

Merge main into the branch or rebase the branch onto main; on the pull request page, Update branch does the merge and Update with rebase does the rebase. GitHub notes you may not be able to use the Update branch button when the pull request’s head branch is itself protected. The up-to-date setting in the starter rule turns this into a requirement on main, which is why I leave it off for small teams.

Can I make a branch private in GitHub?

Not in anything GitHub documents: visibility is set per repository, and a repository can be either public or private. That one branch cannot be made private on its own is my reading, since GitHub’s docs describe visibility only at the repository level. Code that must stay private belongs in a separate private repository.