Rotate first
Once a secret has been pushed, assume someone has a copy. Clones, forks, CI caches and pull request references can all hold the old commits, and public repositories are watched by automated scanners. GitHub's own guide says the first step is to revoke or rotate the secret. After that, removing it from history is cleanup, not the fix. What to do if you leaked an API key covers rotation for common providers.
Find every commit that contains it
gitleaks scans the whole history of a repository:
gitleaks git -v .
If you know the value, or the start of it, git lists every commit that added or removed it:
git log --all -p -S 'sk_test_51FakeKey'
To see what else a suspicious commit exposes, paste the output of git show into PasteSafe. The Findings list shows each masked value next to its placeholder, which helps you build the list of strings to remove. Before highlights what PasteSafe finds, After is its exact output:
commit 3f1c9a7e2b8d4f60c5e9b2d7a1f4c8e93b6d2a10
Author: Sam Lee <sam.lee@example.org>
Date: Tue Sep 15 09:41:07 2026 +0000
add local config
diff --git a/.env b/.env
new file mode 100644
--- /dev/null
+++ b/.env
@@ -0,0 +1,4 @@
+DATABASE_URL=postgres://app:Wint3r-Fake-Pass@localhost:5432/app
+STRIPE_SECRET_KEY=sk_test_51FakeKeyForDocs0nlyNotReal0000
+JWT_SIGNING_SECRET=fake-signing-secret-9f8a7b6c
+LOG_LEVEL=debugcommit 3f1c9a7e2b8d4f60c5e9b2d7a1f4c8e93b6d2a10
Author: Sam Lee <EMAIL_1>
Date: Tue Sep 15 09:41:07 2026 +0000
add local config
diff --git a/.env b/.env
new file mode 100644
--- /dev/null
+++ b/.env
@@ -0,0 +1,4 @@
+DATABASE_URL=postgres://app:DB_PASSWORD_1@localhost:5432/app
+STRIPE_SECRET_KEY=STRIPE_KEY_1
+JWT_SIGNING_SECRET=SECRET_1
+LOG_LEVEL=debugThe commit hash is left alone. The author email is masked too: it is personal data, but not something you need to remove from history.
Rewrite history with git filter-repo
git filter-repo is the tool GitHub's documentation uses. Work in a fresh clone. Put the values to remove in a text file outside the repository, one per line:
sk_test_51FakeKeyForDocs0nlyNotReal0000
Wint3r-Fake-Pass==>REMOVED_DB_PASSWORD
regex:JWT_SIGNING_SECRET=.*==>JWT_SIGNING_SECRET=REMOVED
Each line is literal text unless it starts with regex: or glob:. Matches are replaced with ***REMOVED***, or with the text after ==>. Then run:
git filter-repo --sensitive-data-removal --replace-text ../passwords.txt
To drop a whole file from every commit instead, for example a committed .env:
git filter-repo --sensitive-data-removal --invert-paths --path .env
--sensitive-data-removal needs git filter-repo 2.47 or later. It fetches all refs first and prints the follow up steps for the remote and other clones.
Or use BFG Repo-Cleaner
BFG is an older alternative that many guides still use. It works on a mirror clone and by default does not change your latest commit, so remove the secret from the current files and commit that first:
git clone --mirror https://github.com/acme/billing.git
bfg --replace-text passwords.txt billing.git
cd billing.git
git reflog expire --expire=now --all && git gc --prune=now --aggressive
Push and clean up the copies
- Force push the rewritten branches and tags, following the instructions filter-repo prints.
- Ask everyone with a clone to delete it and clone again. One push from an old clone brings the secret back.
- On GitHub, old commits can stay reachable through forks, pull request references and cached views. GitHub's guide to removing sensitive data from a repository explains when to contact GitHub Support.
- Delete CI caches and build artifacts made from the old commits.
Keep it from happening again
- Add
.envand other local config files to.gitignore, and commit a.env.examplewith names only. - Run gitleaks as a pre-commit hook. Add this to
.pre-commit-config.yaml, using the latest release tag forrev:
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.24.2
hooks:
- id: gitleaks
- Turn on push protection for the repository if your git host offers it.
What PasteSafe does not do
PasteSafe never touches your repository. It masks a copy of the text you paste, which helps when reviewing or sharing a diff, but the history still has to be rewritten with git filter-repo or BFG using the real values.
Questions
Does deleting the file in a new commit remove the secret?
No. The file is gone from the latest commit, but every earlier commit still contains it, and anyone with access can check out or browse those commits. Rotate the secret and rewrite the history.
Should I use git filter-repo or BFG?
Either works. GitHub's documentation uses git filter-repo, which can also remove files by path and prints cleanup steps with --sensitive-data-removal. BFG replaces text on a mirror clone and leaves your latest commit alone by default.
Do I still need to rotate the key after rewriting history?
Yes. Rewriting history does not reach existing clones, forks or anything a scanner already copied. Rotation is what makes the leaked value useless.
Does a force push remove the secret from GitHub?
Not completely. Old commits can stay reachable through forks, pull request references and cached views. GitHub's guide on removing sensitive data explains when to contact GitHub Support.