Secret scanning tells you a credential is in your repository. Push protection tries to stop it getting there.
That difference is the entire reason both features exist, and it is worth stating as sharply as possible:
Secret scanning finds exposure. Push protection tries to prevent it.
The gap between them is an incident. A detected secret has been committed, pushed and pulled by everyone with a clone. A blocked push has not left the developer’s machine.
The short answer
Section titled “The short answer”Push protection blocks pushes and content submissions containing supported secret patterns, across:
- Pushes from the command line
- Commits made in the GitHub web interface
- File uploads to a repository on GitHub
- Requests to the REST API
- Interactions with the GitHub MCP server, on public repositories
When it blocks, the developer either removes the secret — the intended outcome — or bypasses with a stated reason. The three reasons are “It’s used in tests”, “It’s a false positive” and “I’ll fix it later”, and they do not have identical consequences.
Availability mirrors secret scanning: free and available on public repositories, and requiring GitHub Secret Protection on private and internal repositories. Repository-level push protection is disabled by default and has to be turned on.
Separately, push protection for users is enabled by default on personal accounts and stops you pushing secrets to public repositories on GitHub, without generating alerts.
Why prevention is worth so much more
Section titled “Why prevention is worth so much more”The economics are not close.
A blocked push costs the developer two minutes. They remove the value, put it in a secret manager or an environment variable, and push again. Nothing else happens.
A detected secret costs a rotation, a search for everywhere the credential was used, a decision about history rewriting, coordination with everyone who has a clone, and — on a public repository — the assumption that it has already been used by somebody else.
The same credential, the same developer, the same mistake. The only variable is which side of the push it was caught on.
That ratio is why push protection is the highest-value single setting in this cluster, and why the bypass workflow deserves careful design rather than being treated as an escape hatch nobody thinks about.
The surfaces it covers
Section titled “The surfaces it covers”| Surface | Covered |
|---|---|
git push from the command line | Yes |
| Commits made in the GitHub web editor | Yes |
| File uploads through the web interface | Yes |
| REST API requests | Yes |
| GitHub MCP server interactions | Yes, on public repositories |
The web and API coverage matters more than it looks. A control that only intercepts git push is one
that anybody can route around by editing a file in the browser — not maliciously, just because that
was the convenient way to fix a config value.
The three bypass reasons
Section titled “The three bypass reasons”When a push is blocked, a user with write access can proceed by choosing a reason. The reasons differ in what they leave behind, and that difference is the design.
| Reason | Alert created | Meaning |
|---|---|---|
| It’s used in tests | Closed alert | A real credential, deliberately accepted |
| It’s a false positive | Closed alert | Not actually a credential |
| I’ll fix it later | Open alert | A real credential, acknowledged, not yet dealt with |
Two of the three close the alert immediately. The third leaves it open, which is exactly right: “I’ll fix it later” is a commitment, and an open alert is the record of an outstanding one.
“It’s a false positive” should be verifiable. A string that matched a provider pattern but is an example from documentation is a genuine false positive. A string that looks like an example because it is short is not. If you are unsure, treat it as real — the cost of rotating something unnecessarily is minutes.
Delegated bypass
Section titled “Delegated bypass”Individual bypass makes push protection advisory: the person who most wants to get past the block is the person deciding whether to.
Delegated bypass changes that. An organisation designates specific actors who can either approve other people’s bypass requests, or who are exempt from push protection entirely. Everyone else who hits a block must request approval, and somebody else decides.
The security effect is significant and easy to state: it converts bypass from a self-service action into a reviewed one. In practice it also converts it from an invisible event into a conversation, which is usually where the “actually, that is a real credential” realisation happens.
The cost is latency. A developer blocked at 18:00 on a Friday waits for a reviewer. Design for that:
- Make the approver group large enough to always have someone available
- Route requests somewhere with notifications, not to a page somebody visits
- Have a documented answer for the case where nobody is available
A control with no answer for the urgent case gets disabled during the first urgent case.
Push protection for users
Section titled “Push protection for users”Separate from anything a repository configures. It is an account-level setting, enabled by default, that stops you pushing supported secrets to public repositories on GitHub.
Three properties worth knowing:
It generates no alerts. Unless repository-level protection is also active, blocking is all that happens — there is nothing to triage.
It covers you across GitHub, including repositories that belong to other people and have no protection of their own.
It is on unless you turned it off. If you have never seen it fire, that is a good sign rather than evidence it is absent.
This is the reason a first-time open-source contributor is stopped from committing an AWS key to a project that has never configured anything. It is a quietly significant piece of ecosystem protection.
What a block looks like, and what to do
Section titled “What a block looks like, and what to do”The push is rejected before any object is accepted, and the message names the secret type and the location — file, line and commit.
The correct response, in order:
-
Do not bypass reflexively. The block is the cheap moment. Everything after it is expensive.
-
Remove the value from the file. Replace it with a reference to an environment variable or a secret manager.
-
Amend or rewrite the local commits so the value is not in any commit you are about to push. For a single unpushed commit,
git commit --amend. For several, an interactive rebase. -
Rotate the credential anyway if it was ever real. It has been on your disk, possibly in a backup, possibly in shell history.
-
Push again.
Step 3 is the one people get wrong. Removing the value in a new commit does not help — the earlier commit is still in the push, and push protection scans the whole push, not the resulting tree. The value has to come out of the commits themselves.
The threat model
Section titled “The threat model”Threat. A developer commits a working credential and pushes it, making it available to everyone with access to the repository and — on a public repository — to automated scrapers.
Attack surface. Every route by which content enters a repository: the command line, the web editor, file uploads, the API, and any integration acting on someone’s behalf. A control covering only one of those is routed around by accident, not by intent.
Impact. Whatever the credential reaches, from the moment of the push. On a public repository, assume compromise at push time rather than at alert time.
Control. Block at the point of entry for every recognised pattern, on every surface, with bypass that is reviewed rather than self-service.
Verification. Attempt a push containing a correctly shaped fake credential on each surface you care about — command line and web editor at minimum. Then attempt one in your organisation’s own token format. The second test is the one that finds the gap.
Layering with pre-commit scanning
Section titled “Layering with pre-commit scanning”Push protection is server-side, which is both its strength and its limitation. It cannot be skipped by a developer’s local configuration, and it also does not fire until a push is attempted — by which point the credential is in local commit history and has to be rewritten out.
A local pre-commit scan sits in front of it and catches the same class of mistake one step earlier, before there is a commit to rewrite:
repos: - repo: https://github.com/gitleaks/gitleaks rev: v8.30.1 hooks: - id: gitleaksThe trade-off is exactly inverted. A pre-commit hook is trivially skipped — it is not installed on a
new machine, and git commit --no-verify bypasses it deliberately — but when it does fire, the fix
is editing a file rather than rewriting history.
That is what makes them complementary rather than redundant:
| Pre-commit hook | Push protection | |
|---|---|---|
| Runs | On the developer’s machine | On GitHub |
| Fires at | Commit time | Push time |
| Skippable | Yes, easily | Only through a recorded bypass |
| Cost of a hit | Edit a file | Rewrite commits |
| Covers | Whatever you configured | Supported patterns, plus custom ones |
| Fails when | Not installed, or --no-verify | Pattern not modelled |
Neither is sufficient. The failure modes do not overlap, which is the definition of a useful layer. GitLeaks covers the local half properly.
Custom patterns are what make it cover you
Section titled “Custom patterns are what make it cover you”Push protection blocks what secret scanning can match. For an organisation that issues its own credentials — internal service tokens, signing keys, database credentials with a house format — everything GitHub ships is irrelevant to the most likely leak.
Custom patterns close that, and they can be enabled for push protection specifically, so your own formats get blocked at the gate rather than merely reported afterwards.
Two things make a custom pattern work:
A distinctive prefix on the credential. If you issue the tokens, give them one — acme_svc_ costs
nothing at design time and turns an unmatched string into a reliably detectable one. Without a
distinguishing feature, any pattern broad enough to catch the token is broad enough to catch
everything else that shape.
A dry run before enabling. Patterns can be evaluated against existing content first, which tells you the false-positive rate before it becomes everybody’s blocked push. A pattern that fires on ordinary content will be bypassed reflexively within a week, and that habit then applies to the real blocks too.
The order matters: write the pattern, dry run it, enable it for scanning, then enable it for push protection. Skipping to the last step is how a control ends up being disabled.
Rolling it out
Section titled “Rolling it out”Push protection blocks work, so adoption fails when it is enabled without preparation.
-
Enable secret scanning first, and look at the existing alerts. They tell you which credential types your organisation actually leaks.
-
Write custom patterns for your own token formats before enabling blocking, so the control covers the credentials you issue.
-
Enable on new and low-traffic repositories first. Low disruption, real signal.
-
Tell people what will happen and what to do. The failure mode of an unannounced block is a bypass, and the first bypass sets the norm.
-
Turn on delegated bypass once the block rate is low enough that approval is not a bottleneck.
-
Watch the bypass reasons. A high proportion of “used in tests” is a finding about your test credentials, not about the tool.
-
Enable everywhere, and treat “I’ll fix it later” alerts as a queue with an owner.
Step 6 is the one that produces the most value and gets skipped. The bypass reasons are a free survey of how your organisation actually handles credentials, and the answer is usually more interesting than the alert list.
Measuring whether it is working
Section titled “Measuring whether it is working”A control that blocks things produces data, and almost nobody looks at it.
Three numbers are worth tracking, and each one answers a different question.
Blocks per week. Not a problem indicator — a value indicator. Every block is an incident that did not happen. A team seeing three blocks a month is being protected three times a month, and that is the number to quote when somebody asks whether the feature earns its licence cost.
Bypass rate. Blocks that ended in a bypass rather than a fix. A rising bypass rate means the control is becoming advisory. Investigate the cause rather than the people: it is usually one noisy pattern, or a workflow that genuinely needs a credential in a place nobody has provided an alternative for.
Bypass reason distribution. The most informative of the three.
- Mostly “false positive” → a pattern needs tuning, or people are misusing the reason because it is the one that closes the alert with the least friction.
- Mostly “used in tests” → your test infrastructure needs real credentials, which is a finding about architecture rather than about developers.
- Any meaningful volume of “I’ll fix it later” → check that queue. Those alerts stay open by design, and an open alert with no owner is a decision to accept the risk that nobody made explicitly.
The organisation-level view aggregates all of this, which turns “is push protection working?” from an opinion into three numbers you can watch move.
When a legitimate value keeps getting blocked
Section titled “When a legitimate value keeps getting blocked”Occasionally a string that is genuinely not a credential matches a pattern repeatedly — a test fixture, a documented example, a hash that happens to look like a token. Bypassing every time is the wrong answer, because it trains the reflex you need people not to have.
The options, in order of preference:
Change the value. Most example credentials do not need to look realistic. A key written as
YOUR_TOKEN_HERE communicates the same thing to a reader and matches nothing. This is almost always
the right fix, and it is the convention this site uses throughout.
Move it out of the repository. A fixture that must contain something credential-shaped can generate it at test time rather than storing it.
Tune the pattern, if it is a custom one you own. A pattern producing repeated false positives is a pattern that needs a tighter definition.
Bypass with “false positive”, having actually verified that it is one — and expect to do it again next time, because nothing has changed.
The general principle: the sustainable resolution to a repeated block is to remove the thing that looks like a credential, not to keep asserting that it is not one.
Common mistakes
Section titled “Common mistakes”Enabling scanning and not push protection. Detection without prevention means every leak is an incident. The two are separate settings and the second is disabled by default on repositories.
Bypassing with “used in tests” for a real credential. The most common misuse, and the one that converts a blocked push into a closed alert covering a working key.
Removing the secret in a new commit. The earlier commit is still in the push. The value has to come out of the commit that contains it.
Not rotating after a block. The credential was on disk, in an editor buffer, possibly in shell history and a backup. A block is not a guarantee of non-exposure.
Assuming it covers your own token formats. It covers supported patterns. Yours are not among them until you write one.
Leaving individual bypass in place indefinitely. Self-service bypass makes the control advisory for exactly the people most motivated to get past it.
Treating “I’ll fix it later” alerts as closed. They are deliberately left open. If nobody works that queue, the reason is functionally identical to “never”.
Mental model
Section titled “Mental model”Push protection is a gate on the way in, matching known shapes. It converts an expensive incident into a cheap interruption for the credential formats it recognises — and does nothing at all for the ones it does not.
What you learned
Section titled “What you learned”- Push protection prevents; secret scanning detects; the gap between them is an incident
- It covers command-line pushes, web commits and uploads, REST API requests, and MCP interactions on public repositories
- Three bypass reasons exist; “I’ll fix it later” leaves an open alert, the other two close one
- “It’s used in tests” is a claim about what the credential can reach, and is often wrong
- Delegated bypass makes bypass a reviewed action rather than a self-service one
- Push protection for users is on by default and protects public repositories account-wide, with no alerts
- Removing a secret in a new commit does not unblock the push; the commit itself must be rewritten
- Rotate after a block, because the credential existed on disk regardless
- Bypass reason statistics are a free survey of how your organisation handles credentials
Exercise
Section titled “Exercise”Use a public disposable repository and clearly fake values only.
-
Confirm push protection is enabled on the repository.
-
Commit a file containing a string in a recognisable provider format — an obviously invalid
github_pat_value, for instance — and push. Predict: does the push fail, and does the error name the file and line? -
Add a second commit that deletes the file, and push both. Predict: does the push now succeed?
-
Undo that, then use
git commit --amendto remove the value from the offending commit. Push. Predict: the difference between this and step 3 is the lesson. -
Recreate the block and read the bypass options. Note which one leaves an alert open.
-
Check your account settings for “push protection for yourself”. Predict: is it already on?
-
Delete the repository.
Related lessons
Section titled “Related lessons”The secrets management checklist and least-privilege token guide are in the Professional Toolkit.