AI Agent Overwrote .env / Environment Variables

An AI coding agent rewrote .env with placeholders or deleted it. Recover the real values, then hard-lock the file so it never happens again.

You open .env to run the project and every API key is now your_api_key_here, xxx, or replace_me. During a “clean up the project” or “fill in the .env template” pass, Claude Code, Cursor, or Aider treated .env as a template and wiped your real secrets. Worse case: it deleted .env entirely, git wasn’t tracking it, and there’s no local backup.

Fastest recovery (do this first): in VS Code or Cursor, right-click .env in the Explorer and choose Open Timeline, then click the entry from just before the agent’s edit and pick Restore Contents. Local History is on by default (workbench.localHistory.enabled defaults to true as of June 2026) and quietly snapshots every save, so it’s the single most reliable source. If the agent wrote the file through the terminal rather than the editor, see the full recovery table below.

This article covers recovery first, then how to hard-lock the file so the agent never touches it again. The agent ignore files alone are not enough — there are known cases (early 2026) where ignore rules and even permissions.deny are bypassed by terminal or MCP tool calls, so we stack several layers.

Common causes

Ordered by hit rate, highest first.

1. .env is gitignored, so the agent thinks it’s empty or missing

The number-one cause. .env is in .gitignore, so the agent can’t see its contents through git. It reads .env.example full of placeholders, decides to “complete” the missing .env, and overwrites your real values.

# the agent's "completion"
OPENAI_API_KEY=your_openai_api_key_here
DATABASE_URL=postgres://user:password@localhost:5432/db
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxx

How to spot it: Open .env. If every value is your_xxx_here / xxx / textbook example data, this is it.

2. Agent “completed” example values for thoroughness

You asked it to “add a new env var REDIS_URL.” It rewrote the whole file in passing, replacing every existing value with example defaults because it judged that “tidier.”

How to spot it: ls -la .env shows a fresh mtime even though git can’t diff an ignored file.

3. Editor extension or formatter rewrote on save

Some VS Code dotenv extensions “prettify” .env on save — quoting values, aligning =, sorting alphabetically. A few buggy versions blank values entirely. When Cursor’s agent saves the file, those extensions fire too.

How to spot it: Disable dotenv extensions, ask the agent to edit again, and see whether it still mutates the values.

4. Agent confused .env with .env.example

It meant to update .env.example (the committed template) but the command had the wrong path and overwrote .env (your real values) with template content.

How to spot it: Check the agent’s recent Write / Edit tool calls — was the target path .env or .env.example?

5. Agent ran a “generate .env” script

It ran cp .env.example .env, or a project-provided npm run setup script that overwrites unconditionally.

How to spot it: Search the agent’s command history for cp .env, mv .env, > .env, npm run setup, init.

6. Multi-worktree / multi-machine confusion

The agent was working in a git worktree or sub-directory and created a new .env (empty template). When you switched back, the IDE picked up that one.

How to spot it: Run find . -name ".env*" -not -path "./node_modules/*" — multiple .env files mean confusion.

Which bucket are you in

SymptomMost likely causeJump to
All values became your_..._here / xxxCause 1 or 2Restore, then lock
Values got quoted / re-aligned / sorted, still realCause 3 (formatter)Disable extension
.env is gone entirelyCause 4 or 5Timeline / backup
Two .env files exist, one emptyCause 6 (worktree)find then pick the real one

Shortest path to fix

Step 1 is recovery. Steps 2-6 prevent recurrence; stack as many as you can.

Step 1: Restore the real values

Try these sources in order of success rate.

SourceCommand / action
VS Code / Cursor Local HistoryRight-click .envOpen Timeline → pick entry before the edit → Restore Contents
Deploy platform env panelVercel/Netlify/Railway: Project → Settings → Environment Variables → reveal & copy
1Password / Bitwarden / company secret managerOpen the saved secret entry
macOS Time Machinetmutil restore /Users/you/project/.env
Git stash (if you stashed before)git stash list, then git stash show -p 'stash@{0}'
Shell historygrep -i "API_KEY=" ~/.zsh_history (or ~/.bash_history) for past exports
Team Slack / NotionSearch chat history for a shared screenshot or paste

Local History (workbench.localHistory.enabled, on by default) keeps up to 50 entries per file at up to 256 KB each. Its one blind spot: it only snapshots saves made inside the editor. If the agent rewrote .env via a terminal command (cp, >, a setup script), there may be no Timeline entry — fall back to the deploy platform or your secret manager.

Step 2: Add agent ignore files alongside .gitignore

Different agents use different filenames. Cover all of them:

# .cursorignore  (Cursor — full block: no indexing, no agent read, no @-mention)
.env
.env.local
.env.*.local
*.pem
*.key

# .aiderignore  (Aider — uses .gitignore syntax)
.env
.env.*

# .gitignore  (keep this too)
.env
.env.local

Two caveats worth knowing as of June 2026:

  • Cursor also reads .cursorindexingignore, but that only hides files from codebase search — the agent can still read those files when you @-mention them or drag them into chat. For real blocking use .cursorignore, not .cursorindexingignore. Cursor already ignores .env by default, but listing it explicitly is the safe move.
  • Ignore files govern Cursor’s own file access; terminal commands and MCP tools run outside that sandbox and can still read or write .env. That’s why Steps 3-6 matter.

Step 3: Lock it in Claude Code’s permissions.deny

Claude Code reads permissions from .claude/settings.json. A deny rule is stronger than an ignore file because it blocks the Read/Write/Edit tools outright:

{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Write(./.env)",
      "Edit(./.env)",
      "Read(./secrets/**)"
    ]
  }
}

Commit this as .claude/settings.json so the whole team shares it (use .claude/settings.local.json for personal-only overrides; precedence is managed > local > project > user). Two known limitations to plan around:

  • A reported caching bug means a freshly added Read(*/.env*) deny rule sometimes isn’t enforced until settings.json is edited again mid-session — if it leaks once, save the file a second time and restart the session.
  • deny blocks Claude Code’s own file tools, not a cat .env typed into the Bash tool. Add a matching Bash deny if you want belt-and-suspenders, e.g. "Bash(cat ./.env*)".

Step 4: Hard-lock the rule in CLAUDE.md / AGENTS.md / .cursorrules

## File protection

- `.env` and `.env.*` (except `.env.example`) must never be read, edited, deleted, or overwritten
- To add a new env var, update only `.env.example` and list the new keys in the PR description
- Any "clean up project," "standardize config," or "fill in .env" request requires explicit user confirmation
- Real user secrets must never appear in agent output

Paste this into the project-root instruction file. Claude Code, Cursor, Aider, and Codex all read these files every turn — it’s a soft guardrail (the model can still ignore it), so treat it as one layer, not the whole defense.

Step 5: Commit a safe .env.example

Give the agent a legal target so it doesn’t reach for the real .env:

# Generate template from real .env (values stripped)
sed -E 's/=.*/=/' .env > .env.example
git add .env.example
git commit -m "chore: add .env.example template"

Future “add a new env var” requests update .env.example; you sync to .env by hand.

Step 6: Pre-commit hook against accidental commits

Even if the agent tries to commit .env, the hook stops it:

# .git/hooks/pre-commit  (chmod +x it; or wire via husky / lefthook)
#!/usr/bin/env bash
leaked=$(git diff --cached --name-only | grep -E '^\.env(\..+)?$' | grep -v '\.env\.example$')
if [ -n "$leaked" ]; then
  echo "ERROR: refusing to commit .env file(s):"
  echo "$leaked"
  exit 1
fi

(The common one-liner grep -q ... | grep -v is broken: grep -q prints nothing, so the second grep never matches. Capture the matches in a variable instead, as above.)

For stronger coverage, add gitleaks so a renamed file or an inline-pasted secret is still caught:

# install: brew install gitleaks
# in the same pre-commit hook:
gitleaks protect --staged --redact || exit 1

gitleaks scans staged changes for secret patterns (sk_live_*, AKIA*, -----BEGIN PRIVATE KEY-----) regardless of filename. git-secrets is an older alternative if you can’t add gitleaks.

How to confirm it’s fixed

  1. cat .env shows your real values again (no your_..._here).
  2. The app boots and authenticates: npm run dev (or your start command) with no “missing/invalid API key” errors.
  3. Ask the agent to “tidy up the project config” in a throwaway turn — it should now refuse to read or rewrite .env. In Claude Code you’ll see a permission-denied notice on .env; in Cursor the file won’t appear in its context.
  4. Stage .env deliberately and try to commit — the pre-commit hook must reject it.

Prevention

  • List .env* in .gitignore and every agent’s ignore file (.cursorignore, .aiderignore).
  • Add a permissions.deny block for .env in .claude/settings.json (and a matching Bash deny).
  • Write an explicit “never touch .env” rule in CLAUDE.md / AGENTS.md / .cursorrules with concrete paths.
  • Store real secrets in a password manager (1Password / Bitwarden) or your deploy platform’s env panel — never only in a local .env.
  • Periodically back up .env to an encrypted location: an age-encrypted file in a private repo, or a company secret manager.
  • Commit a .env.example template so the agent has a legitimate target for additions.
  • Run a pre-commit hook plus gitleaks to block .env and raw secrets from entering git.
  • When switching to a worktree or sub-directory, confirm which .env you’re editing — avoid keeping multiple copies.

FAQ

Can I get the values back if the agent deleted .env and it was never in git? Often yes. Check the editor Timeline first (Local History snapshots saves even for gitignored files), then your deploy platform’s env panel, then your password manager. If all three are empty, regenerate the keys from each provider’s dashboard — never reuse a key you can’t confirm is intact.

Why does adding .env to .gitignore make this more likely? Because the agent can’t read a gitignored file’s contents through git, so it sees only the placeholder-filled .env.example and “helpfully” fills in the gap. Keep .env gitignored for security, but pair it with the ignore files and permissions.deny so the agent can’t write to it either.

My permissions.deny rule for .env isn’t being respected — bug? Possibly. There’s a known caching issue where a newly added Read(*/.env*) deny rule isn’t enforced until .claude/settings.json is edited again in the same session. Re-save the settings file and restart the session. Also remember deny only blocks Claude Code’s Read/Write/Edit tools — a cat .env in the Bash tool slips through unless you add a Bash deny too.

Is .cursorignore or .cursorindexingignore the right one? Use .cursorignore for secrets. .cursorindexingignore only removes a file from codebase search — the agent can still read it via @-mention. Neither stops Cursor’s terminal or MCP tools, so keep the deploy-platform/secret-manager backup as your real safety net.

How do I stop this without crippling the agent’s normal work? Block only the secret files (.env, *.pem, *.key, secrets/**), not the whole config tree. Commit a .env.example so the agent still has a legitimate place to add new keys, and tell it in CLAUDE.md to update the example and list new keys in the PR.

Tags: #AI coding #Debug #Troubleshooting