Skip to main content
DevOps Insightsguides & answers

GitHub · troubleshooting

push declined due to repository rule violations

GitHub found something that looks like a credential in the commits you are pushing and refused the push. Deleting the line and committing again does not clear it — the secret is still in the history being sent.

First published

New to this? Read what is actually happening first, then come back to the steps.

On this page · 10 sections

Before you start

  • The full output of the refused push. It lists every commit and file path where GitHub found the secret, and a URL to use if you decide to bypass.
  • The commits on your machine, and certainty about whether any of them were already pushed or shared. Rewriting is only safe for commits nobody else has.
  • Access to wherever the credential was issued, such as the cloud console or the token settings page, so you can revoke it and issue a new one.
  • To bypass: being signed in to GitHub as the user who pushed. If no bypass option appears, remove the secret or submit a request for bypass privileges.
  • Only if the secret is already in pushed history: git-filter-repo 2.47 or later, which GitHub's sensitive-data guide requires.

Run this first

step 1 of 3
git log --oneline origin/<branch>..HEAD

Look for The SHA named in the refusal, and its position in the list

Shows exactly which commits are in the push. If there is more than one, the secret is very likely in a commit behind the tip and that is why removing the line did nothing.

Work out which cause you have

A few questions to narrow the list. Every answer ends in a command that confirms or rules the cause out — this cannot see your cluster, so nothing here is a certainty until you have checked.

Narrow it down

0 answered · nothing is sent anywhere

How many commits does git log origin/<branch>..HEAD list?

This is what the push actually contains, which is not the same as what your files currently look like.

Cause space

4 of 4 still possible

  • The secret is in an earlier commit of the same pushCommon
  • The credential is still in the working treeCommon
  • The secret is in history that was already pushedOccasional
  • It is not actually a credentialOccasional

Nothing ruled out yet. Answer the question above and the branches your answer eliminates will strike through here.

Check it with a tool

Or diagnose it manually

In this order. The first command usually contains the whole answer.

  1. Step 1

    git log --oneline origin/<branch>..HEAD

    Shows exactly which commits are in the push. If there is more than one, the secret is very likely in a commit behind the tip and that is why removing the line did nothing.

    Look for The SHA named in the refusal, and its position in the list

  2. Step 2

    git show <sha> --stat

    Confirms the flagged commit and file directly rather than inferring from the working tree, which no longer reflects it.

    Look for The file named in the refusal

  3. Step 3

    git grep -n -I -E '(api[_-]?key|secret|token|password|BEGIN [A-Z ]*PRIVATE KEY)' -- . ':!*.lock'

    Finds other copies before you rewrite anything. Fixing one and pushing into a second refusal is the common sequence.

    Look for The same value in fixtures, samples or configuration

Every cause, and how to fix it

Ordered by how often each one turns out to be the answer.

The secret is in an earlier commit of the same push

Common

You committed the credential, noticed, removed it, and committed the removal. Both commits are unpushed, so both are in the push, and the first still contains the secret. Nothing about the second commit removes it from the first — git history is append-only until you rewrite it.

Confirm

git log --oneline origin/<branch>..HEAD

More than one commit waiting to be pushed. The refusal names a SHA — check whether it is the tip or one behind it

Fix

  • If the secret is confined to commits not yet pushed anywhere, rewrite them: an interactive rebase to edit the offending commit (git rebase -i <COMMIT-ID>~1, the method GitHub documents), or a soft reset to the merge base with the remote and one clean commit.
  • Confirm afterwards that the secret is absent from every commit in the range, not just from the working tree.
  • Rotate the credential regardless. It existed in a commit; treat it as exposed.
git reset --soft "$(git merge-base HEAD origin/<branch>)"
git diff --cached   # the secret must not appear here, and nothing you did not change should show as deleted; fix the file and git add it
git commit -m 'clean commit without the secret'

Collapses unpushed commits into one, letting you stage a version with no secret. Only safe while the commits have not been pushed or shared.

The credential is still in the working tree

Common

It was removed from the file you were looking at and remains in another — a committed .env, a sample config, a test fixture, a lock file, or a notebook that stores output. The refusal names the path; that name is often somewhere you did not expect.

Confirm

git grep -n -I -E '(api[_-]?key|secret|token|password|BEGIN [A-Z ]*PRIVATE KEY)' -- . ':!*.lock'

The secret at the path and line the refusal names, and any other copy of that value in tracked files

Fix

  • Move the value to an environment variable or a secret store and reference it by name, then amend the commit that introduced it (git commit --amend --all), or rebase if it is earlier. A new commit on top is refused again, because the secret is still in the history being pushed.
  • Add the file to .gitignore if it should never have been tracked, and untrack it with git rm --cached.
  • Commit a .env.example with placeholder values so the shape is documented without the secret.

The secret is in history that was already pushed

Occasional

The credential entered the branch some time ago and this push happens to be the first one scanned, or the block is on a branch whose history includes it. Rewriting shared history is a different and more disruptive operation than fixing an unpushed commit, and it does not retract anything already fetched by others.

Confirm

git log --all --oneline -S'<the secret value>' -- .

Commits that introduce or remove the value, and whether any of them are already on the remote

Fix

  • Rotate the credential first. On shared history this is the step that actually helps — the old value must stop working.
  • Follow GitHub's documented process for removing sensitive data if the history genuinely has to be purged, and coordinate with everyone who has a clone.
  • Accept that purging does not undo exposure: anything already cloned or cached keeps the old objects.

It is not actually a credential

Occasional

A test fixture, a documentation example, or a random string that matches a provider's pattern. Real, and less common than it feels in the moment — the instinct on being blocked is to assume the scanner is wrong.

Confirm

git show <sha>:<path> | sed -n '<line>p'

The exact matched value, so you can decide whether it is live rather than whether it looks live

Fix

  • Confirm it is not live before concluding it is a false positive — try it against the provider, or check when it was last used.
  • Where the platform allows it, use the documented bypass and give the honest reason; GitHub records the choice and opens an alert for "I'll fix it later".
  • Prefer obviously fake fixtures — values that cannot be mistaken for real ones by a person or a scanner.

Understanding it properly

Skip this if you are mid-incident — the working part of the page is above. Worth reading afterwards, because understanding the mechanism is what stops the next one.

In plain terms

GitHub looked through the commits you tried to push, found something that looks like a password or access key, and refused the push. The secret is still inside one of those commits even if you deleted it from the file afterwards. You need to rewrite the commits before pushing, and if the secret is real, replace it with a new one.

One secret, three commits

Documented, not reproduced: this is the part of a blocked push that GitHub's command-line guide shows, where the same personal access token appears in three commits. The guide shows only this excerpt of the message.

Command

git push

Output · as shown in GitHub Docs — Working with push protection from the command line

remote:   —— GitHub Personal Access Token ——————————————————————
remote:    locations:
remote:      - commit: 8728dbe67
remote:        path: README.md:4
remote:      - commit: 03d69e5d3
remote:        path: README.md:4
remote:      - commit: 8053f7b27
remote:        path: README.md:4

How to read it

  • The header names the type of secret GitHub matched, here a GitHub personal access token.
  • Each commit line is one place the secret lives in this push. A new commit that deletes the line leaves all three of these in the push, which is why the same refusal comes back.
  • path gives the file and line, README.md:4. Check it for every listed commit, since the path can differ from one commit to the next.
  • Find the oldest listed commit with git log, then follow GitHub's steps for an earlier commit: git rebase -i <COMMIT-ID>~1, edit that commit to remove the secret, git commit --amend, git rebase --continue, and push again.

Words used on this page

Push protection
A GitHub check that scans the commits in a push for secrets and blocks the push if it finds one. It also covers commits made in the web UI, file uploads and REST API requests.
Secret scanning alert
The record GitHub creates for a found secret. Bypassing with I'll fix it later leaves an open alert; It's used in tests or It's a false positive creates one that is already closed.
Bypass
Letting a blocked secret through by visiting the URL in the push output and choosing a reason. You then have three hours to push again.
Delegated bypass
A setting where only chosen people may bypass push protection. Everyone else sends a bypass request for them to review.
Rewriting history
Replacing commits with edited copies: git commit --amend for the latest commit, git rebase -i for an earlier one. The rewritten commits get new SHAs.
Revoke and rotate
Revoking makes the old credential stop working; rotating issues a new one in its place. GitHub's guide to removing sensitive data puts this step first.
Merge base
The newest commit your branch and the remote branch share, from git merge-base HEAD origin/<branch>. Rewriting only the commits after it leaves everyone else's work alone.
What is actually happening

Push protection scans the commits in a push, not the difference between your branch and the remote. That distinction is the whole page. A secret you added in commit three and deleted in commit four is still present in commit three, and commit three is part of what you are pushing.

So the intuitive fix fails. You remove the line, commit, push, and get the identical refusal — which reads like the scanner ignoring you. It is doing exactly what it says: the push still carries a commit containing the secret.

GitHub's documentation describes the block plainly — the push is stopped and a message explains why. Its command-line guide also covers the case above, where the secret sits in an earlier commit of the same push, under "Removing a secret introduced by an earlier commit on your branch".

Treat any secret that reached a commit as compromised, even though the push was blocked. It has been on your disk, in your shell history, and possibly in a backup. Rotation is not optional because the push failed.

How to tell this is your problem
WhereWhat you see
git push"[remote rejected]" and "push declined due to repository rule violations"
The push outputA named secret type, the file, and the commit SHA it appears in
After deleting the lineThe same refusal, naming the same earlier commit

How to know it is actually fixed

  • git log origin/<branch>..HEAD shows only commits you have inspected.
  • git grep for the value returns nothing across the tracked tree.
  • The push completes without a refusal.
  • The credential has been rotated and the old value no longer authenticates. This is the check that matters most, and the only one the push succeeding does not imply.

Stopping it happening again

  • Keep credentials out of the repository entirely: environment variables, a secret manager, or the CI provider's own store.
  • Commit a .env.example with placeholders and add the real .env to .gitignore before the first commit, not after.
  • Run a secret scanner as a pre-commit hook so the block happens on your machine, where rewriting a commit is trivial.
  • For CI, prefer short-lived identity over stored keys — a token that expires in minutes is a much smaller problem when it leaks.

Security considerations

  • Treat a real credential that reached a commit as exposed and rotate it at its provider. The block kept it off GitHub, not out of your local history, backups or any other remote you pushed to, and GitHub's sensitive-data guide puts revoking or rotating first.
  • Never bypass a real secret with It's a false positive or It's used in tests. Both create an alert that is already closed, so nothing prompts anyone to follow up, and the reason you chose stays on record.
  • git log -S'<the secret value>' puts the live secret into your shell history and the process list. Search for a short, distinctive fragment instead, or rotate first so the value you search for no longer works.
  • Rewrite from the merge base, not from origin/<branch>. In a reproduction with Git 2.50.1, git reset --soft origin/main after a fetch staged D TEAM.md, the reversal of a teammate's commit, next to the change being cleaned. Read git diff --cached for unexpected deletions, not only for the secret.
  • Purging history that was already pushed needs coordination. GitHub warns that clones made before the rewrite bring the data back, forks keep it, and removing pull request references and cached views needs GitHub Support.

Sources

Behaviour described here is drawn from official documentation. Where a figure could not be confirmed on an official page it is attributed in the text rather than stated as canonical.

Terms used here (1)

Short definitions for the vocabulary on this page, in case any of it is new. Each links to the full entry.

Secret
A Kubernetes object for storing sensitive data such as passwords, tokens, and TLS certificates. Values in a Secret's data field are base64-encoded, which is an encoding, not encryption: by default Secrets are stored unencrypted in etcd, so encryption at rest and RBAC need to be configured. Secrets can be mounted as files or injected as environment variables, keeping sensitive values out of container images.

All 59 terms in the glossary

Did this get you to an answer?

No text box on purpose — please do not paste production logs anywhere

Something wrong or out of date? Report it