Clean up historyAlways fixable

Remove a password or API key from Git history

I committed a secret — how do I remove it from Git history?

Short answer

Rotate the secret first — that's the only step that genuinely protects you. Then purge it from history with git filter-repo (or the BFG Repo-Cleaner) and force-push. Deleting the file in a new commit does nothing, because the old commit still contains it and is still fetchable.

quick fix
pip install git-filter-repo
git filter-repo --path .env --invert-paths

Strips a path out of every commit that ever contained it

Does this match your situation?

  • You committed a .env, credentials file, private key or hard-coded API token.
  • A secret scanner (GitHub, GitGuardian) flagged your repository.
  • You deleted the file in a later commit but the secret is still visible in history.

Step-by-step fix

  1. Rotate the secret before touching Git

    Assume the credential is compromised the moment it lands on a remote. Bots scrape public repositories within seconds, and history rewrites cannot recall what was already cloned, cached or indexed. Revoking and reissuing the key is the only step that actually restores security — everything after this is cleanup.

    step 1
    1. Revoke the key in the provider's dashboard
    2. Issue a replacement
    3. Store it in an environment variable or secret manager
    4. Only then continue with the history rewrite
    

    If the repo is public, treat the secret as leaked no matter how quickly you rewrite history.

  2. Install git-filter-repo

    This is the tool the Git project itself recommends. The older git filter-branch is officially discouraged: it's dramatically slower and easy to get subtly wrong.

    step 2
    pip install git-filter-repo
    
    # macOS:  brew install git-filter-repo
    # Windows: pip install git-filter-repo  (needs Python on PATH)
    
  3. Back up the repository, then purge the file

    Rewriting history is not reversible in place, so clone a backup first. Then remove the path from every commit that ever contained it.

    step 3
    git clone --mirror . ../repo-backup.git      # safety copy
    
    git filter-repo --path .env --invert-paths
    
    # multiple paths at once:
    git filter-repo --path .env --path config/secrets.json --invert-paths
    
  4. Or redact just the string, keeping the file

    When the secret is one line inside a file you want to keep, replace the text everywhere instead of deleting the whole path.

    step 4
    printf 'sk_live_abc123==>REDACTED\n' > ../replacements.txt
    git filter-repo --replace-text ../replacements.txt
    
  5. Force-push and get everyone onto the new history

    Every commit hash after the rewrite point has changed, so the remote needs the new history and every clone is now invalid. Teammates should re-clone rather than pull, because merging old history back in would reintroduce the secret.

    step 5
    git remote add origin <url>      # filter-repo removes remotes by design
    git push --force --all
    git push --force --tags
    

    On GitHub, also delete stale forks and ask support to purge cached views of the old commits.

Why this works

Git history is an append-only chain of immutable snapshots. Deleting a file in a new commit adds a snapshot without it, but every earlier commit still contains the file verbatim and anyone can check it out. The only way to make the content actually disappear is to rebuild every affected commit without it, which produces entirely new hashes for that commit and all its descendants. Since the old objects may already exist on the server, in forks, in CI caches and in other people's clones, the rewrite is damage control — rotation is the fix.

If that didn’t work

  • The BFG Repo-Cleaner is an easier alternative for the common cases: java -jar bfg.jar --replace-text passwords.txt.
  • For a repository whose entire history is compromised, deleting and recreating it can be cleaner than rewriting.
  • GitHub Support can purge cached commit views that remain reachable by direct URL after a force-push.

How to stop it happening again

  • Add .env and credential paths to .gitignore before your first commit.
  • Enable push protection / secret scanning on GitHub or GitLab.
  • Install a pre-commit hook such as gitleaks or detect-secrets to block secrets locally.
  • Keep configuration in environment variables and commit only a .env.example with placeholder values.

Commands used in this guide

git filter-repo

Rewrite an entire repository's history - the supported way to purge a leaked secret or a huge file.

git push

Upload your commits to the remote repository.

git log

Browse commit history with as much or as little detail as you want.

git rebase -i

Reorder, squash, split, reword or drop your recent commits.

Still stuck?

Search the full command reference and every other rescue guide — there are 56 of them, covering everything from detached HEAD to force-push disasters.

Browse all guides →

Frequently asked questions

Is deleting the file in a new commit enough to remove a secret?

No. The earlier commits still contain the file and anyone can retrieve it with git show or by checking out that commit. You must rewrite every commit that contained the secret, and you must rotate the credential regardless.

Should I use git filter-repo or git filter-branch?

git filter-repo. The Git project explicitly discourages filter-branch — it is orders of magnitude slower on real repositories and has sharp edges that silently corrupt history. filter-repo is the officially recommended replacement.

Do I still need to rotate the key if I rewrote history immediately?

Yes. Automated scrapers monitor public repositories continuously and can grab a key within seconds of the push. The remote may also keep the old objects reachable by direct hash URL, and forks and clones are entirely outside your control.

What happens to my teammates after the rewrite?

Their clones now contain history that no longer exists upstream. They should re-clone. If they pull or merge instead, the old commits — and the secret — get pushed straight back into the repository.

Related rescue guides

← All Git Rescue guides