Quick Answer
A .env file committed to Git exposes every secret it contains - database passwords, API keys, and auth tokens - to anyone with access to the repository. Deleting the file in a new commit is not enough; the secret remains in Git history. You must rotate every exposed credential, add .env to .gitignore, and optionally rewrite history to fully clean the repository.
How Common Is This, and Why Does It Happen with AI Tools?
Committing a .env file is one of the most common security mistakes in vibe-coded projects. When you use Bolt, Lovable, Cursor, or v0 to scaffold a new app, the AI generates a working project structure as fast as possible. That often means creating a .env file with real values, running git init, and pushing everything to GitHub - without ever pausing to configure .gitignore first.
According to GitGuardian's State of Secrets Sprawl 2024, 12.8 million new secrets were leaked in public GitHub repositories in 2023, a 28% increase over 2022. Environment files are a common way those leaks happen. Bots watch public repositories for new secrets, so treat a pushed key as found the moment it lands.
The problem is compounded by the fact that many AI tools generate .env files with placeholder values that developers replace with real credentials and then commit alongside the rest of their work. What started as a template instantly becomes a live security incident the moment real values are added and pushed.
It is a small example of comprehension debt: the agent set up the repository, and nobody read what it committed. A secret scan the agent has to pass would have caught it; see how to make Claude Code or Cursor check its own code.
Even private repositories are not safe. A private repo can be made public by mistake, new collaborators and contractors get access over time, and a team member's compromised account can read everything. Once a secret is in Git history, the exposure grows with every day it remains unrotated.
What Gets Exposed When a .env File Is Committed
A typical .env file in a vibe-coded app contains a collection of secrets that, individually, each represent a significant security risk:
# โ BAD - This entire file should NEVER be committed to Git
DATABASE_URL=postgresql://postgres:mypassword123@db.example.com:5432/mydb
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIn0.XXXX
OPENAI_API_KEY=sk-proj-abc123def456ghi789jkl012mno345pqr678stu
STRIPE_SECRET_KEY=sk_live_51ABC123DEF456GHI789JKL012MNO345PQR678
NEXTAUTH_SECRET=my-super-secret-nextauth-key-that-should-be-random
SENDGRID_API_KEY=SG.abc123def456ghi789jkl012mno345pqr678stu901vwx
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Each of these is a distinct liability. An exposed STRIPE_SECRET_KEY allows attackers to process charges, create refunds, and read customer data. An exposed SUPABASE_SERVICE_ROLE_KEY bypasses Row Level Security entirely and gives full read/write access to your database. An exposed AWS_ACCESS_KEY_ID with a secret key can provision infrastructure and run up thousands of dollars in cloud bills within hours.
Why Deleting the File Is Not Enough
This is the part that catches most developers off guard. Git keeps every commit you make. When you delete .env and push a new commit, the current state of the repository no longer contains the file, but every previous commit that included it still does.
To find secrets already in your history and remove them, see scanning and removing leaked secrets from Git history. To stop the next one before it is committed, add pre-commit secret scanning.
Anyone can retrieve your secrets by running:
# List every commit that touched .env
git log --all --full-history --oneline -- .env
# Print the file from a commit that still had it
git show <commit>:.env
# Or search every change for a key name
git log -p --all | grep -A 5 "STRIPE_SECRET_KEY"
This means that the moment a .env file with real credentials is committed and pushed - even for one second - those credentials must be treated as compromised. The only correct response is to rotate them immediately in the provider's dashboard, not to simply remove the file.
How to Detect a Committed .env File
| Detection Method | What It Finds | Limitation |
|---|---|---|
| Check current repo state | git ls-files | grep .env |
Only shows current working tree, not history |
| Search full Git history | git log --all --oneline -- .env "**/.env" |
Manual, easy to miss patterns |
| Gitleaks scan | All secret patterns in history | Requires installation |
| GitHub Secret Scanning | Known token patterns across all branches and history | Free for public repos; private repos need GitHub Secret Protection (paid) |
| VibeDoctor automated scan | A committed .env at the repo root (PRJ-001), .gitignore gaps (PRJ-002), secrets in any file (Gitleaks) |
Needs repo access; checks the scanned commit, not past commits |
How to Fix a Committed .env File
Follow these steps in order. Skipping step one means the rest is security theater:
Step 1 - Rotate every exposed credential immediately. Go to each provider's dashboard (Stripe, Supabase, OpenAI, AWS, etc.) and generate a new key. Revoke the old one. Do this before anything else.
Step 2 - Add .env to .gitignore. Create or update your .gitignore file at the project root:
# โ
GOOD - Add these lines to your .gitignore
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.*.local
Step 3 - Remove the file from tracking. Even after adding to .gitignore, if the file is already tracked by Git, you must explicitly stop tracking it:
# โ
GOOD - Remove from tracking without deleting the local file
git rm --cached .env
git commit -m "Remove .env from tracking"
Step 4 - Create a .env.example file. This committed placeholder tells other developers which variables they need, without exposing actual values:
# โ
GOOD - .env.example (safe to commit)
DATABASE_URL=postgresql://user:password@host:5432/dbname
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here
OPENAI_API_KEY=sk-your-key-here
STRIPE_SECRET_KEY=sk_live_your-key-here
NEXTAUTH_SECRET=generate-with-openssl-rand-base64-32
Step 5 - Optionally rewrite history. If your repository is public, or if you cannot guarantee the old keys have never been accessed, use git filter-repo to remove the file from all commits. This is a destructive operation that rewrites history - coordinate with your team before running it on a shared repository.
How to Scan for This Issue Automatically
Manual inspection is unreliable, especially on larger projects where dozens of AI-generated files may contain secrets in unexpected places. VibeDoctor's Vibe Check flags a committed .env file at the repo root, as critical when it holds a real secret, and runs Gitleaks on every file so each secret is reported with its file and line. It checks the scanned commit, not past commits, so run the history scan below once. Free to sign up.
For ongoing prevention, run Gitleaks as a pre-commit hook, so a secret is blocked before the commit is made. To check a repository by hand:
# Install Gitleaks
brew install gitleaks # macOS
# or download from github.com/gitleaks/gitleaks/releases
# Scan the files as they are now
gitleaks dir . -v
# Scan every commit on every branch (finds secrets in old commits)
gitleaks git . -v --log-opts="--all"
Vercel, Netlify, Railway, and Render all provide environment variable storage in their dashboards. For production deployments, never rely on .env files - use the platform's secret management system and reference variables via process.env.VARIABLE_NAME in your code.
What a Secure Project Structure Looks Like
A Next.js project built with Cursor or Lovable should have the following environment file setup from the start:
# Files in your repository root
.env.example # โ
Committed - placeholder values only
.env # โ Never committed - real values, in .gitignore
.gitignore # โ
Committed - includes .env pattern
# Vercel / production
# All variables set via: vercel env add VARIABLE_NAME production
When another developer clones the project, they run cp .env.example .env and fill in their own values. No secrets ever travel through Git.
FAQ
Does a .env file get pushed to GitHub?
Only if Git tracks it. git push sends your commits, and a .env file ends up in a commit when you run git add . while it is not listed in .gitignore. Once it is listed, git add skips it. Run git ls-files .env: if it prints nothing, the file is not tracked and will not be pushed.
I deleted the .env file in a new commit - are my secrets safe now?
No. The secrets are still visible in Git history to anyone who runs git log -p. You must rotate every credential that was in the file, regardless of whether you removed the file. Treat those credentials as permanently compromised from the moment they were pushed.
My repository is private - does this still matter?
Yes. Private repositories can be made public by mistake, shared with new collaborators, or read through a team member's compromised account. Any secret in Git history is a liability. If your company is ever acquired or audited, historical commits may be reviewed too.
Does Bolt or Lovable create .gitignore files automatically?
Sometimes, but not reliably. AI tools vary in whether they generate a proper .gitignore and whether it includes the full range of .env patterns. Always verify that .env is explicitly listed before your first git push. git check-ignore .env prints .env when the file is ignored, and git ls-files .env prints nothing when Git is not tracking it.
What is the .env.example file for?
It's a committed template that lists every environment variable your project needs, with placeholder values instead of real secrets. New developers copy it to .env and fill in their own credentials. It serves as living documentation of required configuration without exposing anything sensitive.
How do I set environment variables on Vercel instead of using .env?
In your Vercel dashboard, go to your project settings and select the Environment Variables tab. Add each variable for the Production, Preview, and Development environments as needed. The Vercel CLI also lets you run vercel env add VARIABLE_NAME from the terminal. Your deployed app accesses them via process.env.VARIABLE_NAME exactly as it would locally.