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.

.env
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:6379
app.js
require("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 .env reveals everything.
  • 2.Easy to commit by accident. A missing .gitignore entry, a mistyped filename like .env.production that doesn't match the ignore pattern, or a git 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 .env file 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 .env and .env.example. In practice, these files drift apart within weeks.
  • No guarantee of completeness. There is no automated check that every variable in .env.example has 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 .env file 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:

Terminal
# 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 :3000

Your 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