You committed .env to Git: remove it, then do the part everyone skips

Short answer: revoke and replace every key that was in the file first; that is what makes it safe. Then, if you still want it out of history, run git filter-repo --invert-paths --path .env, force-push, and have everyone re-clone. Rewriting history does not reach clones, forks, cached pages or objects the server keeps, so it can never replace rotation.

If your .env is already pushed, the order matters: the key is the problem, the file is only where you found it. This page gives the exact commands (tested on a scratch repository with git 2.54 and git-filter-repo) and what each step does and does not achieve. If you haven’t yet, start with the leaked-key playbook and rotate before you touch history.

Step 0: rotate first

GitHub’s own documentation says it plainly: for a secret, “as a first step you need to revoke and/or rotate that secret. Once the secret is revoked or rotated, it can no longer be used for access, and that may be sufficient to solve your problem” (GitHub Docs). Bots scan new commits quickly (evidence), so assume the values in that file were copied the moment you pushed.

Step 1: confirm what is in history

Deleting the file in a new commit leaves it in every earlier one. Our scratch repository added .env, committed more work, then removed the file and added it to .gitignore:

$ git log --all --oneline -- .env
a7b87af stop tracking .env (history still has it)
90ce93b oops: add .env
$ git grep -c sk_live $(git rev-list --all)
3da984c...:.env:1
90ce93b...:.env:1

Search for the value, not just the file name: a key may also be in a config file or a script in some commit. git grep over git rev-list --all finds it in every revision.

Step 2: rewrite history with git filter-repo

git filter-repo is the tool GitHub’s documentation recommends; install it separately from Git (for example pip install git-filter-repo). Work in a fresh clone you can afford to lose, and make a backup first.

git filter-repo --invert-paths --path .env
New history written in 0.18 seconds; now repacking/cleaning...
Completely finished after 0.50 seconds.
$ git log --all --oneline -- .env      # nothing
$ git grep -c sk_live $(git rev-list --all)   # no matches

Two things to know. It removes your origin remote on purpose, as a safety measure, so re-add it before pushing. And to scrub a value that lives inside other files, use --replace-text with a file of replacements instead of removing a path (see the filter-repo manual).

Step 3: force-push, then coordinate

git remote add origin [email protected]:you/repo.git
git push --force --all origin
git push --force --tags origin

Branch protection that blocks force-pushes must be turned off temporarily. Commit hashes after the first affected commit all change, so tell everyone before you do this. Per GitHub’s docs, rewriting history has real costs: collaborators with old clones can reintroduce the data if they pull and push, closed pull request diffs break, open pull requests need merging or closing first, and commit signatures are removed.

What the rewrite did not remove

We kept a teammate’s clone made before the rewrite and a bare repository standing in for the server:

--- the teammate clone made before the rewrite still has it:
a7b87af stop tracking .env (history still has it)
90ce93b oops: add .env
--- the server-side repository still has the object until it garbage-collects:
$ git cat-file -t 90ce93b...   ->  commit

A fresh clone of the rewritten repository was clean, but the old objects were still in the other two places. GitHub’s documentation lists where else the data can survive a rewrite: “in any clones or forks of your repository,” “directly via their SHA-1 hashes in cached views on GitHub,” and “through any pull requests that reference them.” You cannot clean other people’s clones; send them the filter-repo instructions or ask them to re-clone. GitHub Support can remove cached views and pull-request references, but its docs say it will only help in cases where “the risk can’t be mitigated by rotating affected credentials,” and forks stay accessible until their owners remove them. In other words, the way to make the leaked key harmless is rotation, and the rewrite is housekeeping.

Step 4: find and change every copy

With the old key dead, the new one has to reach every place that used it: other services, CI variables, preview environments, teammates. That is the slow part. With Dynv you can preview a rotation that finds each copy (including the same value under a different variable name) and changes them in one pass (walkthrough). It does not find copies outside Dynv, such as a different repository, so search for the old value’s prefix there too.

Step 5: stop it happening again

  • Ignore the file (.env in .gitignore, and in .dockerignore) and make it unneeded: keep values in a store and hand them to the process at start (migration guide).
  • Turn on push protection / secret scanning in your Git host, so the next token is blocked before it lands.
  • A pre-commit hook that rejects .env-style files and token patterns.
  • Don’t let tools read it. Agents running as you can read the file; see Can Claude Code read your .env?.

Limits of this guide

  • The rewrite was tested on a scratch repository with a bare “server”. Your host’s retention and caching differ; read your host’s documentation (GitHub’s is linked above).
  • Public exposure is permanent in practice: if the repository was public, assume the content was copied, whatever you do now.
  • git filter-repo rewrites everything it touches. Back up, use a fresh clone, and test on a copy.

Facts last verified . Spotted an error? Tell us.

New here? Start here for the guides in a sensible order.