Stop Committing .env Files to Git
Blog

Stop Committing .env Files to Git

A practical guide to stop committing .env files to Git: gitignore, history cleanup, secret rotation, and encrypted alternatives for managing secrets.

READ MORE

TL;DR: Add .env to .gitignore, rotate any secret that ever touched a commit (ignoring it does not remove it from history), and move shared secrets into an encrypted store instead of plaintext files.

To stop committing .env files: add them to .gitignore, scrub any that are already tracked, rotate the secrets they contained, and stop passing secrets around as plaintext files. The order matters. Ignoring a file does not delete the secrets already sitting in your Git history, so the cleanup and rotation steps are the ones that actually protect you.

This post walks through each step with copy-pasteable commands, then covers what to use instead of committing secrets at all.

Why a committed .env is worse than it looks

A .env file in a commit is not just visible to your team. It is permanently recorded in history, included in every clone, and pushed to every fork and mirror. If the repo is ever made public, or a laptop with a clone is compromised, every key in that file is exposed.

Two properties make this especially dangerous:

  • History is sticky. Deleting the file in a new commit leaves the old version reachable through git log, tags, and branches.
  • Bots are fast. Public repositories are scanned continuously. Leaked cloud keys are often used within minutes of being pushed.

The practical conclusion: once a secret has been committed and pushed, treat it as compromised. Cleanup limits the blast radius, but rotation is what makes the key safe again.

Step 1: Stop tracking the file going forward

Add the file to .gitignore so Git ignores future changes:

# .gitignore
.env
.env.*
!.env.example

The !.env.example line keeps a checked-in template (with empty or dummy values) so teammates know which variables they need without seeing real secrets. You can generate that template automatically from an existing file with the env-example generator.

If the file is already tracked, .gitignore alone does nothing. Untrack it without deleting your local copy:

git rm --cached .env
git commit -m "Stop tracking .env"

Step 2: Check whether the secret is in history

Before you celebrate, find out if the file was ever committed:

git log --all --full-history -- .env

If that prints commits, the secrets in those versions are still recoverable by anyone with the repo. Move to Step 3 and Step 4.

Step 3: Purge it from history (if it was committed)

Rewriting history is disruptive. It changes commit hashes and requires everyone to re-clone or hard-reset, so coordinate with your team first. The cleanest tool is git-filter-repo:

# Remove the file from every commit
git filter-repo --path .env --invert-paths

Then force-push the rewritten history:

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

Important caveats:

  • Force-pushing does not retract data already cloned, cached by your Git host, or sitting in open pull requests. Some hosts retain unreachable commits for a while.
  • This is why purging is necessary but not sufficient. The next step is the one that actually closes the hole.

Step 4: Rotate every exposed secret

Assume every value that ever lived in a committed .env is public, and rotate it: database passwords, API keys, OAuth client secrets, signing keys, webhook tokens. Generate fresh, high-entropy replacements, then update them in each provider and in your deployment environment. A secret generator is handy for the values you create yourself.

Rotation is the only step that is guaranteed to work regardless of who already pulled the old history. Do it even if you are confident the repo was always private.

Step 5: Add a guardrail so it does not happen again

Manual discipline fails eventually. Add a pre-commit hook that blocks secrets before they land:

# .git/hooks/pre-commit (or via the pre-commit framework)
if git diff --cached --name-only | grep -qE '(^|/)\.env($|\.)'; then
  echo "Blocked: attempt to commit a .env file"
  exit 1
fi

Tools like gitleaks or trufflehog go further and scan staged diffs for high-entropy strings and known key formats. Run them in CI too, so a bypassed local hook still gets caught.

What to do instead of committing secrets

.gitignore keeps secrets out of the repo, but it does not solve the real problem: your team still needs to share and sync those values somehow. Passing a .env around over Slack or a shared drive just moves the plaintext to a different insecure place.

The better pattern is to keep secrets out of files entirely and inject them at runtime from an encrypted store:

# Pull the latest secrets for this environment, then run the app
envless run -- npm start

This is the model Envless uses. Variable values are encrypted client-side on your device before they ever reach the server, so the backend stores only ciphertext and never sees plaintext. Secrets sync across local, staging, production, CI, and your whole team, with versioning, transactional publish and rollback, and an attributed change history on every variable. Sign-in is passwordless, so there is no reusable password to leak, and account access on its own reveals no value without the workspace passphrase. There are a typed TypeScript SDK, plus a REST API for every other language plus a REST API when you need to load values programmatically.

The free tier covers one project with no credit card, which is enough to migrate a single repo off committed .env files and see how it feels; the pricing page breaks down team plans if you outgrow it.

The short version

  1. Add .env to .gitignore and untrack it.
  2. Check history; purge with git-filter-repo if it was ever committed.
  3. Rotate every exposed secret. This is non-negotiable.
  4. Add a pre-commit hook plus CI secret scanning.
  5. Replace plaintext file sharing with an encrypted, synced secrets store.

For more on why .env files struggle at team scale, see the rest of the blog.