Pillar 3’s rulesets article treats rulesets as the modern replacement for branch protection: how to create one, how targeting works, how they compare.
This page treats them as policy infrastructure. A ruleset is a statement about what may happen to a set of refs, applied to a set of repositories, with a named set of exceptions — which is close enough to a security policy that the two can finally be the same object.
The short answer
Section titled “The short answer”Rulesets beat branch protection for security work for four specific reasons:
- They layer. Multiple rulesets that target the same ref all apply, and the effective policy is the union of their restrictions. You cannot accidentally weaken a strict rule by adding a broad one.
- They target more than branches. Tag rulesets protect release tags. Push rulesets constrain what can enter a repository at all, across its whole fork network.
- Bypass is an explicit list of actors, not a checkbox about administrators.
- They have an evaluate mode, so you can measure what a policy would block before it blocks anything.
Add that organisation-level rulesets can target many repositories at once — by name pattern, by custom property, or all of them — and rulesets stop being a per-repository setting and start being governance.
The rules, grouped by what they defend
Section titled “The rules, grouped by what they defend”The exact set changes as GitHub ships. This grouping is by security purpose, which is more stable than the interface.
Who may change the ref
Section titled “Who may change the ref”| Rule | Effect |
|---|---|
| Restrict creations | Only bypass actors may create refs matching the pattern |
| Restrict updates | Only bypass actors may push to matching refs |
| Restrict deletions | Matching refs cannot be deleted |
| Block force pushes | History on matching refs cannot be rewritten |
Restrict creations is the one that gets overlooked and is genuinely security-relevant. If a
deployment workflow triggers on release/*, then creating a branch matching that pattern is
triggering a deployment. Restricting creation on the pattern closes a path that branch protection
never covered, because there was nothing to protect until the branch existed.
What must happen before a merge
Section titled “What must happen before a merge”| Rule | Effect |
|---|---|
| Require a pull request before merging | Changes must arrive through a reviewable surface |
| Require status checks to pass | Named checks must succeed |
| Require code scanning results | Merging is blocked when code scanning finds alerts at or above a chosen severity |
| Require code quality results | Merging is blocked when code quality analysis fails or is incomplete |
| Restrict code coverage | Merging is blocked below a coverage threshold |
| Require deployments to succeed | The change must have deployed to named environments |
| Require signed commits | Every commit must carry a verifiable signature |
| Require linear history | Merge commits are rejected on the target |
Require code scanning results is the most security-specific rule GitHub offers, and it is worth dwelling on. It is not “the code scanning workflow passed” — that is an ordinary status check. It is a gate on the alert state: a pull request introducing an alert at or above the configured severity cannot merge, regardless of whether the workflow exited zero.
That difference matters. A scanning workflow that finds twelve problems and exits successfully satisfies a status check and fails this rule. Configuring the gate on results rather than on exit code is the whole point. See GitHub Code Scanning.
What may enter the repository at all
Section titled “What may enter the repository at all”| Rule | Effect |
|---|---|
| Restrict file paths | Pushes touching matching paths are rejected |
| Restrict file extensions | Commits containing matching extensions are rejected |
| Restrict file size | Individual files above a size limit are rejected |
| Restrict file path length | Paths above a character limit are rejected |
These are the push ruleset rules, and they are a different kind of control from everything above: they are evaluated on push, before the content is in the repository, and they apply across the repository and its entire fork network.
The security uses are narrow but real:
- Blocking key material by extension —
*.pem,*.p12,*.pfx,*.key. This is a crude complement to push protection, which matches content patterns rather than filenames. The two fail differently, which is exactly why having both is worth something. - Protecting sensitive paths — preventing anyone from pushing changes to
/.github/workflows/through a route other than a reviewed pull request. - Blocking large binaries, which is mostly hygiene, but a repository nobody can clone is a repository nobody audits.
Metadata restrictions
Section titled “Metadata restrictions”Organisations on a GitHub Enterprise plan can additionally constrain how commit metadata must be formatted — commit messages, committer and author email addresses, and branch and tag names.
The security-relevant use is the email one. Requiring committer email addresses to match your organisation’s domain makes it harder for a commit authored under an arbitrary identity to land on a protected branch. It is weaker than requiring signatures — an email address is still just a field — but it raises the floor, and it composes with signing rather than competing with it.
Branch and tag name patterns are quieter wins: enforcing that release tags match v*.*.* prevents the
class of mistake where a tag named v1.2 and a tag named 1.2 both exist and consumers pin to
different things.
Bypass, expressed properly
Section titled “Bypass, expressed properly”This is the largest practical improvement over branch protection.
Branch protection’s model was a checkbox: “do not allow bypassing the above settings”, which effectively meant “are administrators subject to this?” Rulesets replace it with a bypass list of specific actors — organisation admins, repository admins, specific teams, specific GitHub Apps, or a deploy key.
Three consequences worth planning around:
You can name automation without naming humans. A release bot that needs to push a version bump to a protected branch can be the only bypass entry, with no person on the list.
You can grant bypass “for pull requests only”. An actor can be permitted to bypass when merging a pull request while still being unable to push directly. That distinction is frequently what people actually want and could not previously express.
The list is legible. “Who can step around this rule” is a list you can read, export and review, rather than a property inferred from role assignments.
Layering
Section titled “Layering”Two rulesets targeting the same branch both apply, and the result is the union of their restrictions. This is the property that makes organisation-wide policy workable.
A realistic three-layer arrangement:
Organisation baseline, targeting all repositories, ~DEFAULT_BRANCH:
block force pushes, restrict deletions, require a pull request. Bypass: nobody, or one break-glass team.
Organisation tier policy, targeting repositories with a custom property such as
tier: production: require one approval, require code scanning results at high or above, require
signed commits.
Repository-specific, in an individual repository: additional required status checks that only
that repository has, restricted creation on release/*.
A repository ends up governed by all three. Nobody can weaken the baseline from inside the repository, because a repository-level ruleset can only add restrictions — it cannot subtract them.
That is the inverse of classic branch protection, where a broad rule and a narrow rule competed and one won. The difference is what makes rulesets usable as governance rather than as configuration.
Evaluate mode
Section titled “Evaluate mode”A ruleset can be set to an Evaluate enforcement status: it is not enforced, but contributions that would have violated it are recorded on the Rule Insights page.
This changes how organisation-wide security policy gets adopted. The usual objection to a new rule is “that will break things”, and the usual response is an argument. Evaluate mode replaces the argument with a measurement: run it for two weeks, read the insights, and see exactly which repositories and which contributors would have been blocked.
It also gives you a rollout sequence that does not require a flag day:
-
Create the ruleset targeting a small set of repositories, status Evaluate.
-
Read Rule Insights after a representative period — long enough to include a release, not just ordinary development.
-
Fix what the insights surface. Usually this is a workflow that pushes directly, or a bot that needs a bypass entry.
-
Switch to Active for that set.
-
Widen the target — more repositories, or a property-based selector — back in Evaluate mode.
-
Repeat.
Note the behaviour of status checks in evaluate mode: the check runs on the targeted branch but is not required to pass. That is useful, and it means evaluate mode measures rule violations rather than simulating the full merge experience.
Tag rulesets
Section titled “Tag rulesets”The most commonly missing control in the entire cluster.
Consumers of your software pin to tags. If v2.1.0 can be moved to a different commit, then the
version somebody installed and audited is not necessarily the version that tag resolves to now, and
nothing in their tooling will notice.
A minimal tag ruleset targeting v*:
- Restrict updates — the tag cannot be moved once created
- Restrict deletions — it cannot be removed and recreated elsewhere
- Restrict creations, if only release automation should be cutting tags
That last one converts “anyone with write access can publish a version” into “the release process publishes versions”, which is a meaningful change in a repository where write access is broad.
For a stronger guarantee, immutable releases lock the tag to a commit at publication and generate a release attestation. Tag rulesets and immutable releases solve overlapping problems from different directions; using both is reasonable.
A worked policy for three tiers
Section titled “A worked policy for three tiers”Abstract layering is easier to evaluate against a concrete example. Assume an organisation with three
kinds of repository and a custom property tier set on each.
Tier: sandbox
Section titled “Tier: sandbox”Experiments, spikes, things that will be deleted. The policy exists so that a sandbox cannot quietly become production.
- Block force pushes, restrict deletions on the default branch
- No pull request requirement
- Bypass: repository admins
The point is not to protect the code. It is to make sure that when somebody says “actually this is running in production now”, the repository has a history you can read.
Tier: internal
Section titled “Tier: internal”Services with real users inside the organisation.
- Everything from sandbox, plus:
- Require a pull request with one approval
- Require status checks: the repository’s test job
- Require code scanning results at
highor above - Restrict creations on
release/* - Tag ruleset on
v*: restrict updates and deletions - Bypass: one named release automation App, for pull requests only
Tier: production
Section titled “Tier: production”Anything customer-facing, or holding customer data.
- Everything from internal, plus:
- Require two approvals, with review from Code Owners
- Dismiss stale approvals on new pushes
- Require signed commits
- Require code scanning results at
mediumor above - Require deployments to succeed in
staging - Bypass: empty, plus a documented break-glass team that is not anybody’s daily account
Three organisation rulesets, targeted by property. A new repository gets the right policy the moment
somebody sets tier, and nobody has to remember a checklist.
The design principle worth extracting: the difference between tiers should be a small number of rules, not a different philosophy. If your production policy and your internal policy share nothing, neither will be maintained.
Migrating from branch protection
Section titled “Migrating from branch protection”Both mechanisms can coexist on the same branch, and when they do, the most restrictive combination applies. That makes a gradual migration safe, and it makes an incomplete one confusing — you can end up with a rule enforced from a place nobody is looking.
A migration that avoids that:
-
Inventory what the branch protection rule actually enforces, including whether administrators are exempt. The exemption checkbox is the thing most often forgotten in translation.
-
Recreate it as a ruleset in Evaluate mode. Nothing changes yet.
-
Compare. Rule Insights will show violations that the branch protection rule was already blocking — those are expected. Anything else is a difference between your two configurations, and it is a difference worth understanding before you enforce.
-
Set the ruleset Active. Both are now enforcing, and the union applies.
-
Delete the branch protection rule. Confirm the tests from the previous lesson still fail correctly.
Do not skip step 5 and leave both in place indefinitely. Two overlapping mechanisms means the answer to “why was this blocked?” requires reading two configurations, and the one people forget is the one that surprises them during an incident.
What rulesets do not do
Section titled “What rulesets do not do”Rulesets are the strongest repository-level policy mechanism GitHub offers, which makes it worth being explicit about their edges.
They do not govern who has access. A ruleset constrains what an identity may do to a ref. It has nothing to say about which identities exist, what they can read, or what tokens they hold. That is least-privilege access.
They do not evaluate content. “Require code scanning results” delegates the content question to code scanning. The ruleset only reads the verdict.
They do not apply to forks’ own refs. Push rulesets extend across a fork network for pushes to that network, but a fork remains a separate repository with its own settings.
They do not protect repository settings. Someone with admin can edit the ruleset. The protection on the protection is organisation-level ownership of the ruleset — which is a good reason to define security-critical rules at the organisation, where repository admins cannot reach them.
That last point is the strongest structural argument for organisation rulesets, and it is easy to miss. A rule defined inside a repository is only as strong as that repository’s admin list. A rule defined at the organisation and targeted at the repository is not.
Rulesets as code
Section titled “Rulesets as code”Rulesets are readable and writable through the REST API, which makes them the first repository protection mechanism that can genuinely be version-controlled and reviewed.
{/* Read every ruleset applying to a repository, including inherited org-level ones */}gh api /repos/OWNER/REPO/rulesets --jq '.[] | {id, name, target, enforcement}'{/* Read one ruleset in full, including its rules and bypass actors */}gh api /repos/OWNER/REPO/rulesets/RULESET_IDTwo things this makes possible that were awkward before:
Drift detection. Store the expected ruleset JSON in a repository, fetch the live version on a schedule, and diff. A bypass entry added during an incident and never removed becomes a failing check rather than an archaeological discovery.
Review. Changing a security policy becomes a pull request against a JSON file, reviewed like any other change, rather than an untracked click in a settings page.
This is the same argument as infrastructure as code, applied to the one part of a repository’s configuration that determines whether everything else holds.
Common mistakes
Section titled “Common mistakes”Migrating rules and forgetting bypass. Branch protection’s admin exemption and a ruleset’s bypass list are different models. A careless migration can produce a ruleset that everybody can bypass.
Assuming a repository ruleset can loosen an organisation one. It cannot, and this is a feature. If a repository genuinely needs an exception, it belongs in the organisation ruleset’s bypass list or its targeting, where it is visible.
Requiring code scanning results without a scanning workflow. The rule gates on alerts. If no analysis runs, there are no results, and the behaviour is not what people assume — check what your configuration does before relying on it.
Protecting branches and ignoring tags. Still the most common gap, and the one with the largest downstream blast radius.
Using name-pattern targeting as policy. It encodes governance in a naming convention that nothing enforces. Custom properties are the durable version.
Leaving a ruleset in Evaluate indefinitely. Evaluate mode is a measurement phase. A ruleset that has been evaluating for a year is a policy nobody decided about, with a dashboard nobody reads.
Mental model
Section titled “Mental model”A ruleset is a policy statement — these rules, on these refs, in these repositories, with these named exceptions. Because rulesets combine by union, adding one can only ever make the policy stricter, which is what makes them safe to apply from the top down.
What you learned
Section titled “What you learned”- Rulesets layer by union; a repository ruleset can add restrictions but never remove organisation ones
- Restrict creations closes deployment paths that only exist once a branch is created
- Require code scanning results gates on alert state, not on a workflow’s exit code
- Push rulesets evaluate on push, apply across the fork network, and can block files by extension, path, size and path length
- Bypass is an explicit actor list, and can be granted for pull requests only
- Metadata restrictions on commits, emails, branches and tags require a GitHub Enterprise plan
- Evaluate mode measures what a policy would block, turning rollout arguments into data
- Tag rulesets are the most commonly missing control, and consumers depend on tags
- Rulesets are readable and writable via the API, so drift detection and review become possible
Exercise
Section titled “Exercise”Use a disposable repository, ideally inside a test organisation.
-
Create a branch ruleset targeting the default branch: restrict deletions, block force pushes, require a pull request. Set it Active.
-
Add yourself to the bypass list. Predict: can you now force-push? Try it, then remove yourself and try again.
-
Create a second ruleset targeting the same branch that additionally requires one approval. Predict: does it replace the first ruleset or combine with it? Open a pull request and read which requirements appear.
-
Create a tag ruleset on
v*restricting updates. Pushv1.0.0, then try to move it withgit tag -f v1.0.0 && git push --force origin v1.0.0. Read the rejection. -
Create a push ruleset restricting the
.pemextension. Try to commit and push a file namedexample.pemcontaining the textNOT A REAL KEY. Predict: at which point does it fail — commit, or push? -
Run
gh api /repos/OWNER/REPO/rulesetsand read the JSON. Identify the bypass actors field. -
Delete the repository.
Related lessons
Section titled “Related lessons”Get the repository security checklists — setup, rulesets, tokens — from the Professional Toolkit.