PasteSafe

How to remove a secret from git history

First rotate the secret, because rewriting history does not stop it from working. Then find every commit that contains it, rewrite the history with git filter-repo or BFG, and clean up the copies a force push does not reach.

Mask your log in PasteSafe

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:

Before
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=debug
After PasteSafe
commit 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=debug

The 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

  1. Force push the rewritten branches and tags, following the instructions filter-repo prints.
  2. Ask everyone with a clone to delete it and clone again. One push from an old clone brings the secret back.
  3. 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.
  4. Delete CI caches and build artifacts made from the old commits.

Keep it from happening again

  • Add .env and other local config files to .gitignore, and commit a .env.example with names only.
  • Run gitleaks as a pre-commit hook. Add this to .pre-commit-config.yaml, using the latest release tag for rev:
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.

Clean it before you paste it

PasteSafe masks API keys, passwords and personal data in your browser. Nothing is uploaded.

Mask your log in PasteSafe