Removing a leaked secret from Git history Jump to heading
A committed credential is an incident, and the instinct to fix it with git rm and a tidy commit is exactly wrong: deleting a file in a new commit changes what the tree looks like now while leaving the secret readable in every earlier commit with a single git show. Removing it properly requires a history rewrite — but the rewrite is not the remediation. This recipe follows the sequence that actually ends the exposure, applying the rewriting mechanics from History Rewriting & Recovery in the right order.
When to use this approach Jump to heading
- An API key, private key, database password or token was committed and pushed.
- A secret-bearing file —
.env, a service-account JSON, a keystore — was added before.gitignorecovered it. - A pre-push secret scan fired after the fact, or a platform scanner reported a finding.
- The repository must be made public and history has to be clean before that happens.
- If the secret was committed but not pushed, this is much simpler: rotate anyway if it left your machine in any form, then rebase or amend locally with no coordination at all.
Step 1 — Rotate the credential before touching Git Jump to heading
# There is no Git command for this step. In your provider's console or CLI:
# 1. Issue a replacement credential
# 2. Deploy the replacement everywhere it is used
# 3. Revoke the leaked one
# 4. Review access logs for use of the leaked credential since it was committed What changed: the string in history is now worthless. Everything that follows is hygiene, and it can proceed at a sensible pace.
Step 2 — Find every commit and every ref that contains it Jump to heading
# Which commits introduced or removed the string, across ALL refs
git log --all --oneline -S'AKIAIOSFODNN7EXAMPLE'
# Which paths have ever contained it
git log --all --name-only --format='%H' -S'AKIAIOSFODNN7EXAMPLE' \
| grep -v '^$' | grep -v '^[0-9a-f]\{40\}$' | sort -u
# Is it present in tags as well as branches?
git for-each-ref --format='%(refname)' refs/tags \
| while read -r t; do
git grep -q 'AKIAIOSFODNN7EXAMPLE' "$t" 2>/dev/null && echo "present in $t"
done What changed: nothing — but --all is doing essential work here. A search that only covers the current branch routinely misses the secret on a release branch or an old tag, and a rewrite scoped to what that search found leaves it in place.
Step 3 — Purge it from all history Jump to heading
Work in a mirror clone. It is faster, it cannot disturb anyone’s working tree, and it keeps the original as a fallback.
# 1. A fresh mirror to rewrite in
git clone --mirror https://example.com/org/repo.git repo-rewrite.git
cd repo-rewrite.git
# 2a. If a whole file must go, remove the path from every commit
git filter-repo --path config/credentials.json --invert-paths
# 2b. If the secret is a string inside files that must stay, replace it
printf 'AKIAIOSFODNN7EXAMPLE==>REDACTED\n' > /tmp/replacements.txt
git filter-repo --replace-text /tmp/replacements.txt What changed: every commit on every ref has been rewritten to exclude the path or redact the string, and every SHA from the earliest affected commit onward is new.
# The rewrite report names what it changed
git count-objects -vH
git log --all --oneline -S'AKIAIOSFODNN7EXAMPLE' | head # expect no output SAFETY WARNING —
filter-repodeliberately removes theoriginremote after rewriting, so a straygit pushcannot publish a half-finished rewrite. Do not add it back until Step 4 passes. When you do push, the target ref is almost certainly protected against rewrites — use the documented, logged override rather than disabling the protection.
Step 4 — Verify the secret is absent from every ref Jump to heading
# 1. Nothing on any ref contains the string
git log --all --oneline -S'AKIAIOSFODNN7EXAMPLE' | wc -l # expect 0
# 2. Nothing in any tag's tree
git grep 'AKIAIOSFODNN7EXAMPLE' $(git tag) 2>/dev/null | head # expect no output
# 3. The old objects are actually gone from this mirror
git reflog expire --expire-unreachable=now --all
git gc --prune=now
git count-objects -vH
# 4. The current tree still builds — a rewrite should not change the present
git --work-tree=/tmp/verify checkout-index -a -f --prefix=/tmp/verify/ Step 5 — Publish and coordinate the re-clone Jump to heading
# Push every rewritten ref, once verification passes
git remote add origin https://example.com/org/repo.git
git push --force --all origin
git push --force --tags origin Then the human half, which is where these migrations actually fail:
# What each colleague runs to confirm they are clean
git rev-parse HEAD # matches the announced SHA
git log --all --oneline -S'AKIAIOSFODNN7EXAMPLE' | wc -l # expect 0 in their clone too The reason the “re-clone, not pull” instruction has to be repeated is that pulling appears to work. Git happily merges the colleague’s old history into the new one, restoring the secret, and nothing warns anyone. Verifying each clone is the only way to know the migration finished.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Why rotate before rewriting instead of after? Jump to heading
Because the rewrite takes hours of coordination and the credential is exposed the entire time. Rotation ends the exposure in minutes and is entirely within your control, whereas the rewrite can never reach forks, clones, CI caches or platform indexes. Rewriting is cleanup; rotation is the remediation.
Is deleting the file in a new commit enough? Jump to heading
No. Deleting a file adds a commit in which the file is absent; every earlier commit still contains it, and anyone can read it with git show. That is the entire reason a rewrite is needed. A deletion commit is worth making anyway so the current tree is clean, but it must not be mistaken for remediation.
What if the repository is public and has forks? Jump to heading
Assume the secret is permanently public and act accordingly: rotate immediately, then check whether the credential was used in the interval. A rewrite cannot reach forks, and hosted platforms may retain the commit through their own references even after your push. Ask the platform to expire cached views, but do not let that request delay rotation.
Related Jump to heading
- History Rewriting & Recovery — the parent guide, including how to take a backup you can return to.
- Blocking Secrets with a Pre-Push Scan — the check that stops the next one before it leaves a laptop.
- Rotating a Compromised Commit Signing Key — the same rotate-first discipline applied to signing material.