Git

How to Remove Sensitive Data From Git Commit History Completely

4 min read by DebuggedIt

Quick answer

You committed an API key, password, or .env file and pushed it β€” deleting it in a new commit isn't enough, because the secret is still sitting in every earlier...

You committed an API key, password, or .env file and pushed it β€” deleting it in a new commit isn't enough, because the secret is still sitting in every earlier commit in the repo's history. Anyone with access to the repo (or its public GitHub history) can dig it out with git log.

The Problem

Deleting the file and committing looks like it fixed things:

$ git rm .env
$ git commit -m "remove env file"
$ git push origin main

But the secret is still recoverable from history:

$ git log --all --full-history -- .env
commit 9e8f7g6a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e
    Author: you
    Date:   Mon Jul 14 10:22:03 2026
        add env config

$ git show 9e8f7g6:.env
AWS_SECRET_ACCESS_KEY=AKIAxxxxxxxxxxxxxxxx

If it's a public repo, tools scan GitHub continuously for exposed keys, and services like AWS often auto-revoke or flag credentials found this way β€” so speed matters here.

Deleting the file vs. rewriting history git rm + commit (not enough) secret removed secret still readable in old commit git filter-repo (rewrites all commits) secret scrubbed from every commit hash

Why It Happens

Git stores every commit as an immutable snapshot. A new commit that deletes a file only affects the latest snapshot β€” it doesn't touch the objects from earlier commits, which stay in .git forever (or until they're garbage collected after being unreferenced). Common ways secrets end up committed:

  • An .env file wasn't added to .gitignore before the first commit.
  • A hardcoded API key or password was pasted directly into source code for a quick test and forgotten.
  • A config file with credentials was committed as a "just this once" shortcut.
  • A previous team member's local Git config leaked into a committed file by accident.

The Fix

The reliable modern tool for this is git filter-repo (the older git filter-branch is officially discouraged β€” it's much slower and easier to misuse). Install it first:

pip install git-filter-repo

Clone a fresh copy of the repo to work on (filter-repo expects a clean clone, not your working copy):

git clone --mirror https://github.com/youruser/yourrepo.git
cd yourrepo.git

Remove the file from every commit in history:

git filter-repo --path .env --invert-paths

If the secret was pasted inline in code rather than in its own file, replace the exact string across all commits instead:

echo 'AKIAxxxxxxxxxxxxxxxx==>REMOVED' > replacements.txt
git filter-repo --replace-text replacements.txt

Push the rewritten history, forcing every branch and tag to update:

git push origin --force --all
git push origin --force --tags

Every collaborator must now re-clone or hard-reset their local copy β€” the commit hashes have all changed:

git fetch origin
git reset --hard origin/main

Still Not Working?

Rewriting history removes the secret from the repo's commit graph, but it does not undo any exposure that already happened. Rewriting the past doesn't rotate the key β€” anyone who already cloned, forked, or cached the repo (including GitHub's own caches and any CI logs) may still have the old value. Treat the credential as permanently compromised and rotate it at the source regardless of whether the history cleanup succeeds:

# Example: rotate an AWS key
aws iam create-access-key --user-name yourservice
aws iam delete-access-key --access-key-id AKIAxxxxxxxxxxxxxxxx --user-name yourservice

Once the history has been rewritten and pushed, there's a bit of cleanup left on GitHub's side specifically. GitHub caches old commits for a period even after a force-push, and any open pull requests referencing the old commits may still expose the secret in their diff view. Close and recreate any pull requests that touched the affected file, and consider reaching out to GitHub support to request a cache purge if the repository was public and the exposure window matters for compliance reasons.

To prevent a repeat of this in the future, add the credential file pattern to .gitignore before it can ever be committed again:

echo ".env" >> .gitignore
git rm --cached .env

It's also worth installing a pre-commit hook that scans for common secret patterns before they ever reach a commit, catching the mistake locally instead of after a push. A simple option is gitleaks, which can run as a pre-commit hook or in CI:

gitleaks detect --source . --verbose

Running this in CI on every pull request catches accidental credential commits from any contributor before they merge, which is far cheaper than a history rewrite after the fact.