Concept
The Problem with .env Files
The .env file has been the default way to manage application secrets for over a decade. It was never designed for security. It was designed for convenience — and that convenience has become one of the most common sources of credential leaks in modern software development.
How dotenv Works
The dotenv library (and its equivalents in every language) follows a simple pattern: at application startup, read a file called .env from the project root, parse each KEY=value line, and inject the values into the process environment.
DATABASE_URL=postgresql://admin:s3cret@db.example.com:5432/myapp
STRIPE_SECRET_KEY=sk_live_abc123def456
JWT_SECRET=my-super-secret-signing-key
REDIS_URL=redis://:password@cache.example.com:6379require("dotenv").config();
// Secrets are now available on process.env
const db = new Pool({ connectionString: process.env.DATABASE_URL });
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);The pattern is straightforward and works everywhere. The problem is not the library — it's the file. A plaintext file containing every credential your application needs, sitting in your project directory, is a liability.
Why .env Files Are Insecure
The risks of .env files are not theoretical. They are structural flaws in the pattern itself:
- 1.Plaintext on disk. The file is unencrypted. Any process running on the machine can read it. This includes malicious npm packages, compromised CI runners, and shared development servers. A single
cat .envreveals everything. - 2.Easy to commit by accident. A missing
.gitignoreentry, a mistyped filename like.env.productionthat doesn't match the ignore pattern, or agit add .in a new subdirectory — and your production credentials are in version control. - 3.No encryption at rest. Even if the file never gets committed, it sits on every developer's laptop unencrypted. A stolen laptop, an unprotected backup, or a compromised cloud dev environment exposes every secret.
- 4.AI agents read them. Cursor, Claude Code, GitHub Copilot, and Windsurf read your entire project directory for context. A
.envfile in the project root is context. The agent ingests your API keys, copies the pattern, and may reproduce real values in generated code or prompts. - 5.No access control or auditing. There is no mechanism to restrict which team members see which secrets, no log of who accessed the file, and no way to revoke access without changing every value.
The Real-World Impact
GitHub's secret scanning service reports millions of leaked secrets on public repositories every year. The majority are credentials that were stored in .env files or hardcoded in configuration files and accidentally committed.
The consequences are immediate and severe:
AWS key compromise
Bots continuously scrape public repositories for AWS access keys. A leaked key pair can be exploited within minutes to spin up cryptocurrency mining instances, access S3 buckets, or exfiltrate data. Victims regularly report bills exceeding $10,000 before the key is revoked.
Database exposure
A committed DATABASE_URL with credentials gives an attacker direct access to your production data. No additional exploit needed — the connection string is the key.
Stripe and payment fraud
Leaked Stripe secret keys allow attackers to issue refunds, create charges, or access customer payment information. The financial and regulatory consequences compound quickly.
Lateral movement
One leaked secret often leads to others. A compromised API key reveals internal service endpoints. A database credential exposes stored tokens. Attackers chain leaked secrets to escalate access across an organization.
What .env.example Doesn't Solve
The common mitigation is to commit a .env.example file with placeholder values and add .env to .gitignore. This helps, but it does not solve the underlying problems:
- •Manual synchronization. When a developer adds a new variable, they must remember to update both
.envand.env.example. In practice, these files drift apart within weeks. - •No guarantee of completeness. There is no automated check that every variable in
.env.examplehas a corresponding real value in the developer's local.env. Missing variables cause runtime errors that are hard to trace. - •Still plaintext on disk. The real
.envfile is still unencrypted. The .env.example pattern does nothing to protect the actual credentials. - •Still shared over Slack. New developers still need to get the real values from someone. The .env.example tells them what keys they need, not how to securely obtain the values.
A Better Approach
The fix is not a better .env file. The fix is removing the file entirely and replacing it with encrypted secret storage and runtime injection. With secr, secrets are stored encrypted on the server and injected into your process at startup — no file written to disk, no plaintext to leak:
# Import your existing .env file into secr (one-time migration)
secr migrate .env
# Delete the .env file — you won't need it again
rm .env .env.local .env.production
# Run your application with secrets injected at runtime
secr run -- npm start
# Loaded 14 secrets into process.env → server running on :3000Your application code does not change. It still reads process.env.DATABASE_URL. The difference is that the value comes from encrypted storage instead of a plaintext file, and there is nothing on disk for an attacker, a malicious package, or an AI agent to read.
Catch What's Already Leaked
Before you migrate, audit your codebase for secrets that have already been committed or left on disk. secr scan detects 20+ patterns including AWS keys, Stripe keys, database URLs, OpenAI tokens, and generic high-entropy strings:
$ secr scan
Found 2 potential secret(s)
.env.backup
[HIGH] AWS Secret Access Key L4:1
AKIA************MPLE
docker-compose.yml
[HIGH] Database URL with Password L12:8
post********************5432
2 high, 0 medium, 0 low | 98 files scanned in 61ms
Tip: Run `secr guard install` to prevent committing secrets.
Replace .env files today
npm i -g @secr/cli
secr migrate .env
secr guard install
secr run -- npm start