Skip to main content

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.

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.

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, or a soft reset to before it 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 origin/<branch> && git restore --staged <file> && 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 value named in the refusal, in any tracked file

Fix

  • Move the value to an environment variable or a secret store and reference it by name.
  • 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.

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. What it does not walk through is the case above, where the secret sits in an earlier commit of the same push, and that is the case most people are actually in.

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.

Did this get you to an answer?

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

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.

Related