GitTOOL-SPECIFICORG-SPECIFIC

Protected Branches

Rules on the branch pointer that delivery reads from — enforced server-side, because anything enforced on the developer's machine is advice.

The question, the obvious approach, and why it breaks

Every lesson starts where the work starts: an operational problem, a first attempt that is entirely reasonable, and the way production disagrees with it.

The production question

What has to be true about the branch your pipeline deploys from, and where can that be enforced?

The problem

If a pipeline deploys whatever a branch points at, then the branch pointer is production-relevant state. Anyone who can move it can change production, and a mistake in moving it is a production incident with no review and no verification.

What teams do first

The team agreed not to push directly to main and to always open a change proposal. Everyone is sensible; a convention is enough.

How it breaks

Conventions are followed until the day someone is in a hurry, and the day someone is in a hurry is exactly the day a change reaches production unreviewed.

How it breaks in production
  • Conventions are followed until the day someone is in a hurry, and the day someone is in a hurry is exactly the day a change reaches production unreviewed.
  • Local hooks are trivially bypassable — a flag on the commit command skips them — and they are not installed on CI runners or on a new laptop at all (CI Security).
  • A single force push to the deploying branch can move production to a commit nobody reviewed, and the previous state is only recoverable if someone still has it.
  • Automation tokens do not follow conventions. A bot with write access will do exactly what it is told, including a force push in a broken workflow.
  • With no protection there is no record of who merged what, which removes the audit trail exactly where blast radius is largest (The Audit Trail).
CodeBuildTestArtifactReleaseDeployRunObserveOperateIncidentRecoverLearnImprove

What is actually happening

Underneath the tooling, which is the part that survives a change of tool.

  • Protection is enforced by the hosting platform on the server side, at the point a ref update is received. That is the only place enforcement is real, because the client is under the author's control.
  • The rules operate on ref updates. Reject non-fast-forward updates stops force pushes. Reject deletes stops branch deletion. Require the update to come from a merge of an approved proposal stops direct pushes.
  • Approval requirements bind to a commit. If the branch moves after approval, whether the approval still counts is a policy setting — and treating a stale approval as current is a real gap (Review as a Gate).
  • Ownership routing — CODEOWNERS-style files — makes "who must approve this" a property of the paths changed rather than of who happens to be around.
  • Linear history requirements reject merge commits, forcing rebase or squash. That is a history-shape choice with real consequences for bisecting, not a safety property.
  • Signed-commit requirements verify that commits carry a valid signature, which answers who authored them. That is a different question from who approved the merge, and both are worth answering under a threat model that includes account compromise (The Delivery Chain as Attack Surface).
  • Every platform has a bypass — administrators, or an explicit override. A bypass that is silent is a hole; a bypass that is loud and recorded is a break-glass mechanism (Break-Glass Access).

What each rule actually stops

Protection settings are usually adopted as a bundle. Each rule addresses a specific failure, and knowing which one addresses which lets you configure deliberately rather than by copying a template.

RuleWhat it rejectsFailure it preventsWhat it does not prevent
No direct pushRef updates not from an approved mergeUnreviewed code reaching the deploying branchA bad change that was reviewed and approved
No force pushNon-fast-forward updatesProduction moving to a commit nobody reviewed; broken commit-to-artifact identityA bad commit pushed forward normally
No deletionRef deletesDelivery stopping, or a reconciler pruning on a missing sourceAnything about the branch's content
Required approvalsMerges without N approvalsChanges with no second readerRubber-stamping (Review as a Gate)
Dismiss stale approvalsMerges where approval predates the current headApproval that refers to code that will not shipA reviewer re-approving without re-reading
Owner approval requiredMerges without the owning team's approvalCross-team changes landing without the people who will be pagedAn owner approving under time pressure
Required status checksMerges with failing or missing checksKnown-broken code on the deploying branch (Required Checks)Defects no check looks for
Require signed commitsCommits without a valid signatureCommits forged under someone else's identityA legitimately-signed bad commit
Linear historyMerge commitsNothing safety-relevant; a history-shape preferenceAny correctness or access problem

Server-side or it is advice

TOOL-SPECIFICOn a hosted platform this is a settings surface. On a self-hosted plain Git remote the equivalent is a pre-receive hook you write and maintain, with no approval model, no ownership routing and no audit UI — the enforcement point exists, the surrounding machinery does not.

The single most important property is where the rule is evaluated. Anything checked on the developer's machine is a convenience: the developer controls the machine, the check is skippable, and a fresh clone has none of it installed.

Local hooks are still worth having — catching a secret before it is committed is much cheaper than rotating it afterwards. They are a fast feedback mechanism, not a control.

The same policy, two places
Client-side convention
a pre-commit hook in the repo
  - installed manually, per developer
  - absent on a fresh clone
  - absent on CI runners
  - skipped by --no-verify
  - not applied to bot accounts
  => the policy holds until someone is in a hurry
Server-side enforcement
a protection rule on the ref
  - evaluated when the update is received
  - applies to every client and every token
  - bypass is explicit, recorded and alertable
  - the configuration itself is reviewable
  => the policy holds, and exceptions are visible

The client is under the author's control and the server is not. That is the whole difference, and it is why local hooks belong in the fast-feedback category alongside editor linting rather than in the control category alongside required checks.

Protection failures

These are mostly configuration failures rather than attacks, and they share a shape: the setting exists, and it is not covering the thing that matters.

TriggerSymptomCauseResponse
Pipeline deploys from a branch that was never protectedUnreviewed commit in productionProtection applied to main by habit while delivery reads another refDerive the protected ref list from the pipeline definition, not from convention
Administrator force-pushes to fix a mistakeDeployed commit no longer exists in history; artifact-to-commit mapping breaksAdministrators exempt from protection by defaultInclude administrators; provide a recorded break-glass path instead (Break-Glass Access)
Bot account granted bypassAutomated commits reach production with no reviewBypass granted once to make a workflow function and never revisitedGive automation the narrowest scope that works; review bot permissions on the access cycle (Access Review)
Approval given, then more commits pushedCode merges with approval that predates itStale approvals not dismissed on new commitsDismiss on push, or require the branch to be current before merge
Protection settings changed quietlyA rule everyone assumed was on has been off for weeksSettings are not themselves under reviewDefine protection as code; alert on drift between definition and platform (Policy as Code)
Emergency bypass during an incidentProtection still disabled weeks laterRe-enabling was nobody's explicit taskMake re-enabling an owned recovery step with a deadline (What Happens Between the Page and the Postmortem)

How to do it properly

Most important first.

  • Protect every branch delivery reads from: the deploying branch first, then release branches, then anything a reconciler watches.
  • At minimum: no direct pushes, no force pushes, no deletion, and at least one approving review from someone with context.
  • Dismiss stale approvals when the branch moves, or require the branch to be current before merging — otherwise approval refers to code that is no longer what will ship (Required Checks).
  • Route approval by ownership, so the approver is someone who will be paged for the result (The Ownership Record).
  • Define protection as code and apply it the same way you apply infrastructure, so it is reviewable and reproducible rather than clicked into a settings page (Policy as Code).
  • Make administrator bypass loud: alert on it, record it, and review it afterwards. Bypass will be needed occasionally; unrecorded bypass is the failure.
  • Audit who has write access on the same cycle as production access, because it is production access (Access Review).

How much can this affect

Every production change has a blast radius. Stated as a scale so it is comparable between changes rather than adjectival — and paired with what actually contains it, because a wide scope with a real containment mechanism is a different situation from a wide scope with none.

Blast radius if this is wrongEveryone
One testEveryone
What contains it

The protection rules themselves, plus whatever deployment strategy sits downstream. An unprotected deploying branch means one push reaches everyone at pipeline speed with no review.

What can go wrong

Failure modes, including of the mitigation
  • Protection configured on main while the pipeline actually deploys from release, which nobody protected.
  • Administrators exempt by default, so the people with the most reach have the least enforcement — and the exemption is invisible in the settings summary.
  • Bot accounts granted bypass to make automation work, creating a permanent unreviewed path to production.
  • Approval requirements set so high that people route around them with emergency access, converting a gate into an incident pattern (Break-Glass Access).
  • Protection settings changed without review, because the settings themselves are not covered by any protection.
  • Signed-commit enforcement adopted without key management, so onboarding breaks and the rule is disabled a month later.
Misreads this invites
  • "Protected means the history cannot change." It means specific ref updates are rejected by that platform. Objects were always immutable; pointers were always mutable (Source Control as Production Infrastructure).
  • "We have branch protection, so main is safe." Protection controls how commits arrive. Whether they are any good is what checks and review are for (Required Checks).
  • "Administrators should be exempt so they can fix things fast." The people with the most reach benefit most from the guardrail. Use a recorded break-glass path instead (Break-Glass Access).
  • "Local hooks enforce our policy." They are conveniences. They are skippable, absent on fresh clones, and absent in CI (CI Security).
  • "Requiring linear history is a safety feature." It is a history-shape preference. It makes bisecting tidier and prevents nothing that force-push protection does not already prevent.

Operating it

Evidence is the signal, not the intention. Rollback is sometimes 'you cannot, and that is the point'.

How you know it worked
  • A push of an unreviewed commit to the deploying branch is rejected — verified by trying it, not by reading the settings.
  • A force push to the deploying branch is rejected, including for administrators, or produces a recorded, alerted bypass.
  • The protection configuration is in version control and matches what the platform actually enforces.
  • The list of accounts that can bypass protection is short, current and unsurprising — including automation.
  • Every commit on the deploying branch is reachable from an approved merge.
How you get back
  • Protection rules are configuration and are reversible in minutes; loosening them during a genuine emergency is legitimate if it is recorded and re-tightened.
  • What is not reversible is a force push that already happened. The overwritten commits survive only in local clones and in reflogs until garbage collection, and reconstructing from those is manual and error-prone.
  • If protection has to be bypassed during an incident, treat re-enabling it as an explicit recovery step with an owner — otherwise it stays off, which is a common and quiet outcome.
What to automate, and what stays human
  • Automate: applying protection settings from a definition in version control, and detecting drift between the definition and the platform.
  • Automate the alert on bypass, on protection changes, and on new write access grants.
  • Keep human: deciding who may bypass, and approving the emergency use of a bypass. That decision is the control (The Automation Trap).
What this costs
  • Protection adds latency to every change, including the trivially safe ones, and there is no reliable automated way to tell those apart in advance.
  • Strict rules push urgent work toward bypass paths. A gate that is too strict does not increase safety, it moves the traffic (Guardrails, Not Gates).
  • Signed commits raise the bar against account compromise and add key distribution, rotation and onboarding work that someone has to own.
  • Enforcement lives in the hosting platform, so it is a dependency on that platform's policy engine and on your ability to configure it correctly.

Where this applies

This domain is unusually tool- and organisation-dependent. These labels say what each claim is specific to, and what a different platform, provider or organisation does instead.

  • TOOL-SPECIFICBranch protection is a hosting-platform feature, not a Git feature. GitHub, GitLab and Bitbucket offer overlapping but differently-named rule sets with different bypass semantics. A plain Git remote over SSH has none of this — equivalent enforcement there requires server-side hooks such as pre-receive, which you write and maintain yourself.
  • ORG-SPECIFICHow many approvals, whether administrators may bypass, and whether commits must be signed are policy choices driven by threat model and compliance obligations. A regulated environment may mandate signed commits and separation of duties; a five-person startup gets most of the value from force-push protection plus one review, and more rules there mostly buy latency.

Where the depth lives

This domain teaches delivery and operation, and hands the mechanism off to the domain that owns it.

Domains that do not exist yet
  • Testing & Reliability Engineering — what the checks bound to these rules should actually establish before a merge is allowed.