# A .env file leaked into Git: cleanup in the right order

> Accidentally committed a .env file? Reissue the values first, then rewrite history, then stop it happening again. What scanners catch and what to record.

URL: https://crunu.com/blog/env-file-leaked-in-git
Language: en
Updated: 2026-10-30

If you pushed a `.env` file with real values, treat every value in it as exposed from the moment of the push, and fix it in this order: reissue each value at its provider, then remove the file from Git history, then put guards in place so it doesn't happen again. Rewriting history first is the common mistake. It takes longer, it can't reach clones and forks, and the leaked keys keep working the whole time. GitHub's own guidance starts with the same step: revoke or rotate the secret first.

This post goes through each step, what push protection and secret scanning catch, and what to write down afterwards. If you're still deciding which env files belong in a repository at all, start with [should you commit .env files](https://crunu.com/blog/should-you-commit-env-files).

## First: did it leave your machine?

If you committed the file but haven't pushed, the values never left your laptop through Git. Remove the file from the commit, add it to `.gitignore`, and commit again:

```sh
git rm --cached .env
echo ".env" >> .gitignore
git commit --amend --no-edit
```

If the bad commit is further back but still unpushed, an interactive rebase or a reset does the same job. Check with `git log --stat origin/main..HEAD` that nothing with secrets is left before you push.

Everything below is for the case where the commit reached a remote.

## Assume it's already copied

Once a commit is pushed, its contents can spread faster than you can react.

*Figure: Diagram of where a secret goes once it is in a Git repository. Git host: the repository, pull requests and forks. Commit history: old commits keep the value after the file is deleted; it stays after you delete it. CI caches and artifacts: copies of the checked-out files. Every clone: laptops and build machines that pulled it; it stays after you delete it. Everyone with read access: today and anyone added later. Deleting the copy doesn’t undo the leak, so the value has to be reissued at its provider.*

Each collaborator who fetched has it. CI may have cloned it, cached it or printed it into a log. Forks have it. On a public repository, automated scrapers look for credentials in new pushes. Whether or not anyone actually used the values, you can't prove they didn't, so the safe assumption is that they were seen.

That assumption sets the order. The only step that makes a leaked value harmless is replacing it at the provider. Everything else limits further spread.

## Step 1: reissue every value, starting with the dangerous ones

Make a list of every variable in the leaked file, then sort it by what an attacker could do with it: payment keys and cloud credentials with broad permissions first, then production databases, then everything else.

For each one, use the order that avoids an outage:

1. Issue a new value at the provider while the old one still works.
2. Deploy it everywhere the old one was used.
3. Confirm the services run on the new value.
4. Revoke or delete the old value.

Some providers allow two valid credentials at once for this reason; the post on [rotating API keys without downtime](https://crunu.com/blog/rotate-api-keys-without-downtime) explains the overlap window. Steps for specific providers are in the [key rotation guides](https://crunu.com/rotate), for example [AWS](https://crunu.com/rotate/aws) and [Stripe](https://crunu.com/rotate/stripe).

While you're at each provider, look at its access logs for the period between the push and the revocation. Unexpected calls, new resources or charges are the difference between "exposed" and "used", and they decide who else needs to know.

Don't skip values that look harmless. A webhook signing secret lets someone forge events to your app. A session signing key lets someone mint sessions. A database URL often contains the password.

## Step 2: remove the file from history

Once nothing in the file works any more, clean up the repository so the old values stop circulating. GitHub notes that after revoking, rewriting history "may not be warranted" at all; you may still do it to keep the old values from being found and tried later.

GitHub recommends `git-filter-repo`, at least version 2.47, for this. To remove a file from every commit:

```sh
git-filter-repo --sensitive-data-removal --invert-paths --path .env
git push --force --mirror origin
```

Use the full path in the repository, and add another `--path` for each place the file lived if it was moved or renamed ([GitHub: removing sensitive data](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository)).

Know what the rewrite does and doesn't reach:

- **It changes commit hashes** of the commit that added the file and of every commit after it. Open pull requests are best merged or closed first.
- **Old clones can bring it back.** A collaborator who pulls and pushes from an old clone can re-push the data. Everyone needs to re-clone or rebase onto the rewritten history.
- **Copies elsewhere stay.** GitHub warns that the commits "may still be accessible elsewhere": in clones and forks, in cached views reached by commit hash, and through pull requests that reference them. Only GitHub Support can remove cached views and pull request references, and forks keep the data until their owners remove it.

This is why history rewriting is cleanup, not remediation.

## Step 3: make it hard to do again

Three layers, each catching what the one before misses.

**Ignore rules.** Ignore all env files and allow only the template:

```text
.env
.env.*
!.env.example
```

**Push protection.** GitHub's push protection "blocks pushes that contain secrets before they reach your repository." It recognizes provider patterns from its partner program, plus generic patterns such as private keys and database connection strings when those are turned on. Two limits matter. At the repository level it "is disabled by default" and requires GitHub Secret Protection; the per-user setting, on by default, covers pushes to public repositories. And "anyone with write access to the repository can bypass push protection by specifying a bypass reason", with the bypass recorded in the audit log ([GitHub: about push protection](https://docs.github.com/en/code-security/secret-scanning/introduction/about-push-protection)).

**Secret scanning.** Scanning looks backwards: it "scans your entire Git history on all branches" and raises alerts. It runs automatically for free on public repositories; private ones need GitHub Secret Protection. For partner secrets found by scanning, GitHub notifies the provider "so they can take action, such as revoking the credential" ([GitHub: about secret scanning](https://docs.github.com/en/code-security/secret-scanning/introduction/about-secret-scanning)). That is useful, but don't count on a provider revocation as your fix: you still need the new value deployed.

What scanners miss is anything without a recognizable shape: an internal service password, a random string your app uses as a signing key, a value split across lines. Pattern-based tools are built around known key formats, so a secret without one is harder for them to match. The glossary entry on [secret scanning](https://crunu.com/learn/secret-scanning) covers how these tools match.

## Step 4: tell the right people and write it down

Who to tell depends on what the values could reach. Your security contact or team lead in every case. The owner of each affected service. If the values could reach customer data, whoever handles incidents and legal obligations in your company, because some regulations have notification deadlines that start when you become aware.

Keep a short record while it's fresh:

- the repository, branch and commit, and when it was pushed;
- the list of values in the file;
- for each value, when it was reissued and when the old one was revoked;
- what the provider logs showed between push and revocation;
- whether history was rewritten, and whether forks or support requests were involved.

The record is what you'll want if one of the old values turns up months later. The glossary entry on [leaked secrets](https://crunu.com/learn/leaked-secret) has the short version of this checklist.

## The longer fix: values that never sit in the repository

A `.env` file leaks into Git because it sits in the working directory, next to the code, one `git add .` away from a commit. The durable fix is to keep only the template there.

With Crunu, the values live in an environment such as `payments-api / development`, encrypted on the devices of the people who have access; the server holds ciphertext and metadata such as names, never the values. A developer starts the app with `crunu run --env payments-api/development -- npm run dev`, and the variables arrive in the process environment instead of a file. The repository keeps `.env.example`, and there is no `.env` with real values to commit by accident.

*Figure: The history of the payments-api production environment at Kiteline, a fictional company: versions 15 to 11, each with its author, time and device, and what changed by variable name, never by value. Version 15 is current. Version 13 is open, and a dialog asks to restore it: Crunu will save a new version 16 with the files of version 13, and version 15 stays in the history.*

When you reissue values after a leak, you save the new ones into the environment as a new version. **History** keeps the last 100 versions with who changed what, by variable name, never by value, so the record from step 4 has a matching trail. If a new key turns out to be wrong, **Restore…** brings back an earlier version as a new one, and with `crunu run --restart on-change` the service restarts on the corrected values. For well-known services, each file shows a **Where to issue a new key** link.

Two things Crunu does not do. It doesn't scan repositories for secrets; keep push protection or a scanner for that. And it doesn't reissue anything at providers; step 1 is still yours. The guide on [sharing .env files with a team](https://crunu.com/guides/share-env-files-with-a-team) covers the setup in full.

## Sources

- [Removing sensitive data from a repository, GitHub Docs](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository), checked October 30, 2026.
- [About push protection, GitHub Docs](https://docs.github.com/en/code-security/secret-scanning/introduction/about-push-protection), checked October 30, 2026.
- [About secret scanning, GitHub Docs](https://docs.github.com/en/code-security/secret-scanning/introduction/about-secret-scanning), checked October 30, 2026.
