How to stop committing .env files to git (and what to do instead)
It happens to everyone. You’re moving fast, you add a new environment variable,
and somewhere between git add . and git push you forget that .env is
supposed to be ignored. Now your database password is in your git history forever.
This post covers how to prevent it, how to fix it if it’s already happened, and what a better long-term workflow looks like.
Why .gitignore isn’t enough
The standard advice is simple: add .env to your .gitignore file. And it works
— until it doesn’t.
The problems:
It only works if the file was never tracked. If you committed .env before
adding it to .gitignore, git will keep tracking it. .gitignore only ignores
untracked files. Running git rm --cached .env fixes this but most developers
don’t know that step.
It has to be set up on every project, every time. One project where you forget
and you have a leak. One new developer who clones the repo on a fresh machine and
doesn’t have your .gitignore habits and you have a leak.
It doesn’t protect against .env.production, .env.staging, .env.local.
You might have .env in your .gitignore but forget about the variants. These
get committed all the time.
It doesn’t help when someone else on your team slips up. You can be disciplined about this all you want. One teammate having a bad day and you’re rotating keys.
How to fix it if it’s already happened
If you’ve already committed secrets, .gitignore won’t help — the damage is done.
Here’s what to do:
Step 1 — Rotate your credentials immediately. Before anything else, go to every service where you use those credentials and generate new ones. Treat the old ones as compromised. This is non-negotiable — even if you scrub your git history, anyone who cloned the repo before you noticed already has the old keys.
Step 2 — Remove the file from git history.
Use git filter-repo (the modern replacement for git filter-branch):
# Install git-filter-repo
pip install git-filter-repo
# Remove the file from all history
git filter-repo --path .env --invert-paths
This rewrites your entire git history. You’ll need to force push and everyone on your team will need to re-clone the repo.
Step 3 — Add it to .gitignore properly.
echo ".env" >> .gitignore
echo ".env.*" >> .gitignore
echo "!.env.example" >> .gitignore
git add .gitignore
git commit -m "ignore env files"
The !.env.example line keeps your example file tracked so teammates know
what variables are needed without seeing the actual values.
Step 4 — Tell GitHub to rescan. Even after scrubbing history, GitHub’s secret scanning may still have cached the credentials. Contact GitHub support to request a rescan, and check if any forks exist that you can’t control.
The .env.example pattern
The best practice most teams use is committing a .env.example file with all
the variable names but no values:
# .env.example — commit this
DATABASE_URL=
STRIPE_SECRET_KEY=
SENDGRID_API_KEY=
NODE_ENV=development
PORT=3000
New developers clone the repo, copy .env.example to .env, fill in the values,
and they’re running. The variable names are documented, the secrets are never
committed.
It works reasonably well for solo projects. The problems start when you have a team and need to share the actual values securely, keep them in sync across environments, and know when someone changed something.
The global .gitignore
One underused trick is a global .gitignore that applies to every git repo on
your machine:
git config --global core.excludesfile ~/.gitignore_global
echo ".env" >> ~/.gitignore_global
echo ".env.*" >> ~/.gitignore_global
This means even if you forget to set up .gitignore in a project, your machine
will never commit .env files. Good backup layer.
Pre-commit hooks
Another layer of protection is a pre-commit hook that scans for secrets before
they’re committed. Tools like git-secrets or detect-secrets can catch
common patterns:
# Install detect-secrets
pip install detect-secrets
# Initialize a baseline
detect-secrets scan > .secrets.baseline
# Add a pre-commit hook
Or use the pre-commit framework with the detect-secrets plugin. This won’t
catch everything — custom API keys with no recognizable pattern will slip through
— but it catches the common cases like AWS keys, Stripe keys, and private keys.
The real fix — don’t use .env files for shared secrets
All of the above is damage control. The root problem is that .env files are
local, unencrypted, and manually synced. As long as secrets live in files on
developer machines, they’ll end up in git eventually.
The more robust solution is keeping secrets out of the filesystem entirely. Tools like EnvMaster store your variables encrypted in the cloud and inject them directly into your process at runtime — no file on disk, nothing to accidentally commit.
envmaster project my-api
envmaster environment production
envmaster run -- node server.js
Your process gets the right variables injected before it starts.
process.env just works. There’s no .env file to commit because there’s
no .env file at all.
Summary
If you haven’t committed secrets yet:
- Add
.envand.env.*to.gitignoreright now - Set up a global
.gitignoreas a backup - Consider a pre-commit hook for extra protection
- Use
.env.exampleto document variable names without values
If you have committed secrets:
- Rotate your credentials immediately
- Use
git filter-repoto scrub history - Force push and have your team re-clone
- Contact GitHub support to rescan
Long term — consider moving secrets out of files entirely. The .env pattern
works until it doesn’t, and when it doesn’t the consequences are real.
EnvMaster keeps secrets out of your filesystem entirely. Try it free — no credit card required.