Back to Blog
Security6 min readMay 15, 2026

Why You Should Never Commit .env Files to Git

I've watched a .env file slip into a commit more than once, usually because .gitignore got added a commit or two late. Once that file lands in the history, deleting it from your working directory does nothing to undo the exposure.

A Password in Plain Text, Searchable Forever

Picture a rushed hotfix at the end of the day. git add ., a quick commit message, push. The .gitignore for this project never got written, so the diff looked like this:

commit 4a1f9c2 - "fix login bug"
+ DATABASE_URL=postgres://admin:Tr0ub4dor@prod-db.internal:5432/app
+ STRIPE_SECRET_KEY=sk_live_51H8xJ2Kd9mLqRt...
+ JWT_SECRET=a3f8b1c9d2e4f5a6b7c8d9e0f1a2b3c4

Security researchers reported over 12 million leaked secrets on GitHub in 2023, a large share of them database credentials and API keys sitting in committed .env files. GitHub's own secret scanning flags patterns like this within minutes of a push, and bots scraping public repos are often faster than that.

What happens next depends on what leaked. A live Stripe key gets used for fraudulent charges before you notice. A database URL turns into a full data dump on a paste site. If the exposed data belongs to your users, you're also looking at breach notification obligations under GDPR or similar laws, and those come with real fines, not warnings.

How It Actually Happens

The scenario above almost never starts with someone deliberately committing a secret. It starts with a .gitignore that doesn't exist yet, on day one of a project, before anyone has thought about it. The first few commits go in clean by luck, then someone adds a .env file to hold a local database URL and it goes in with everything else.

Adding .env to .gitignore later doesn't retroactively fix anything either. Git keeps every version of a tracked file in its history, so if .env was committed once, it stays recoverable from that history even after you stop tracking it going forward. Removing it for good requires rewriting history.

Some IDEs make this worse with auto-stage or auto-commit features that pick up every changed file in the working directory. Without a correct .gitignore in place first, that includes whatever secrets happen to be sitting there.

And a common one: copying .env.example to .env, filling in real credentials for local development, then forgetting that the copy needs its own ignore rule because .env.example being safe to commit says nothing about .env.

Keeping Secrets Out of the Repo

.gitignore Before the First Commit

Write .gitignore before writing any code, not after. Cover every .env variant your project might grow into, even the ones you don't need yet:

# Environment files
.env
.env.local
.env.*.local
.env.development
.env.production
.env.staging

A Pre-commit Hook as a Backstop

.gitignore only helps if it existed before the file was staged. A pre-commit hook catches the case where it didn't. Tools like git-secrets, detect-secrets, or gitleaks scan staged changes and block the commit if they match known secret patterns:

# Install git-secrets
brew install git-secrets
# Configure for your repository
git secrets --install
git secrets --register-aws

Ship .env.example Instead

Commit a .env.example with placeholder values so teammates know what variables the project needs, without any of them being real:

# .env.example - Safe to commit
DATABASE_URL=postgres://user:password@localhost:5432/dbname
API_KEY=your-api-key-here
JWT_SECRET=generate-a-random-string-at-least-32-chars

Audit the Repo Once in a While

Even with good habits, run a scan against your history occasionally with truffleHog or GitHub's secret scanning. Old commits from before you cared about any of this are still there:

# Scan entire Git history
trufflehog git file://path/to/repo

What to Do If You've Already Committed Secrets

If you find secrets already sitting in your repository, the order you handle this in matters more than how fast you do it.

  1. Rotate the exposed credentials first.Do this before touching Git history. A private repository doesn't change the calculus here. Treat every exposed secret as compromised from the moment it was committed.
  2. Remove the secrets from Git history using git-filter-repo or BFG Repo-Cleaner:
    bfg --delete-files .env
    git reflog expire --expire=now --all
    git gc --prune=now --aggressive
  3. Force push to all branches, after coordinating with your team since it rewrites shared history:
    git push --force --all
  4. Notify affected parties if the exposed credentials protected user data.

Skip .env Files in Production Entirely

The safest .env file in production is the one that doesn't exist. Every major host gives you somewhere to store secrets that isn't a plaintext file on disk:

  • Vercel - Environment Variables in Project Settings
  • AWS - Secrets Manager or Parameter Store
  • Google Cloud - Secret Manager
  • Azure - Key Vault
  • Docker/Kubernetes - Docker Secrets or K8s Secrets
  • HashiCorp Vault - for teams that need fine-grained access policies across many services

Encryption at rest, access controls, audit logs, rotation without a redeploy: a plain .env file offers none of it. The setup cost for a real secrets manager is an afternoon. Cleaning up after a leaked production database usually costs a lot more than that, in time and in trust.