你打开 .env 准备跑项目,发现里面所有的 API key 都变成了 your_api_key_here、xxx、replace_me——Claude Code、Cursor 或 Aider 在某轮”清理项目”或”补全 .env 模板”时,把你真实的密钥全擦了。或者更糟:它把 .env 直接删了,git 也没追踪,本地没有备份。这种事故每周都在发生,因为 agent 默认 .env 是模板而非真值。这篇先讲怎么恢复,再讲怎么彻底锁死让它别再碰。
常见原因
按命中率从高到低排序。
1. .env 没在 git 里追踪,agent 以为它是空的或缺失
这是头号原因。.env 在 .gitignore 里,agent 通过 git 看不到内容,它读 .env.example 看到一堆占位符,于是”补全”出一个 .env——直接覆盖了你的真值。
# agent 的"补全"结果
OPENAI_API_KEY=your_openai_api_key_here
DATABASE_URL=postgres://user:password@localhost:5432/db
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxx
如何判断:打开 .env,看 value 是不是全成了 your_xxx_here / xxx / 教科书示例值。
2. Agent “为了完整性”自作主张写示例值
你让它”加一个新的 env 变量 REDIS_URL”,它顺手把整个文件重写了一遍,所有现有值都被它替换成默认示例值。它认为这样”更规范”。
如何判断:git 没追踪 .env 的话看 diff 不出来,但看 mtime 能确认刚被修改:ls -la .env。
3. 编辑器扩展或 formatter 保存时重写
VS Code 的某些 dotenv 扩展会在保存时”美化”.env——把值用引号包起来、对齐 =、按字母排序,有些 bug 版本直接把 value 清空。Cursor 的 agent 触发保存时也会触发这些扩展。
如何判断:禁用 dotenv 相关扩展,看 agent 编辑后是否还会被改。
4. Agent 把 .env 和 .env.example 写反了
它本来要更新 .env.example(提交模板),结果命令打错或路径写错,把 .env(你的真值)覆盖成了模板内容。
如何判断:看 agent 最近的 Write 工具调用,target path 是 .env 还是 .env.example。
5. Agent 跑了 “generate .env” 类脚本
它跑了 cp .env.example .env 或某个项目自带的 npm run setup,那些脚本会无条件覆盖。
如何判断:搜 agent 命令历史里有没有 cp .env、mv .env、> .env、npm run setup、init。
6. 多人 / 多机器同步问题
Agent 在 worktree 或 sub-directory 里干活,它创建了一个新的 .env(空模板),你切回去时 IDE 看到的是它创建的那个。
如何判断:find . -name ".env*" -not -path "./node_modules/*" 看是否有多个 .env。
最短修复路径
按优先级排序。先 Step 1 抢救,再 Step 2-5 防止再次发生。
Step 1:先从备份 / 密码管理器 / 部署平台还原
按”成功率从高到低”试这几个来源:
| 来源 | 命令 / 操作 |
|---|---|
| macOS Time Machine | tmutil restore /Users/you/project/.env |
| Git stash(如果你之前手动 stash 过) | git stash list git stash show -p stash@{0} |
| 1Password / Bitwarden / 公司密码管理器 | 找之前存的 secret 条目 |
| Vercel / Netlify / Railway env 面板 | 项目设置 → Environment Variables → 复制 |
| 团队 Slack / Notion 上次共享的截图 | 翻聊天记录 |
~/.zsh_history / shell history | grep -i "API_KEY=" ~/.zsh_history 找曾 export 过的 |
| IDE 最近文件 / Cursor / VS Code Timeline | 右键 .env → Timeline / Local History |
VS Code 的 Local History(workbench.localHistory.enabled)默认开启,会自动保存最近改动版本——这是最常被忽略但最有效的恢复来源。
Step 2:立即在 .gitignore 旁边写一个 agent ignore
不同 agent 用不同文件名,全写上:
# .cursorignore (Cursor)
.env
.env.local
.env.*.local
# .claudeignore (Claude Code 部分版本)
.env*
# .aiderignore (Aider)
.env
.env.*
# .gitignore 也保留
.env
.env.local
让 agent 在读文件阶段就看不到 .env,从根本上断掉它去”补全”的能力。
Step 3:在 CLAUDE.md / AGENTS.md / .cursorrules 写死规则
## 文件保护
- `.env` 和 `.env.*`(除了 `.env.example`)严禁读取、编辑、删除、覆盖
- 需要新增环境变量时,只能更新 `.env.example`,并在 PR 描述里列出新增的 key
- 任何"清理项目"、"标准化配置"、"补全 .env"类操作都必须先问用户
- 用户的真实密钥永远不应该出现在 agent 的输出里
把这段直接复制进项目根的 instruction 文件。Claude Code、Cursor、Aider、Codex 都会在每轮读这些文件。
Step 4:提交一份”安全”的 .env.example
让 agent 有事可干而不去碰真 .env:
# 从真 .env 生成模板(值清掉)
sed -E 's/=.*/=/' .env > .env.example
git add .env.example
git commit -m "chore: add .env.example template"
之后 agent 想加新变量时,更新的就是 .env.example,你再手动同步到本地 .env。
Step 5:加 pre-commit hook 防止误提
哪怕 agent 真把 .env 提交了,hook 拦下来:
# .git/hooks/pre-commit 或用 husky
if git diff --cached --name-only | grep -qE '^\.env(\.|$)' | grep -v '.env.example'; then
echo "ERROR: refusing to commit .env file"
exit 1
fi
更稳妥用 git-secrets 或 gitleaks 扫描 secret 模式(sk_live_*、AKIA*、-----BEGIN PRIVATE KEY-----),即便文件名变了也拦得住。
预防建议
- 在
.gitignore和每种 agent 的 ignore 文件(.cursorignore、.aiderignore、.claudeignore)里都列出.env* - 在 CLAUDE.md / AGENTS.md /
.cursorrules里写明”严禁触碰 .env”,列出具体路径 - 真实密钥存在密码管理器(1Password / Bitwarden)而不是只存在本地
.env - 定期把
.env备份到加密位置——macOS 的 Vault、age加密的私有 git repo 或公司 secret manager - 提交
.env.example作为模板,让 agent 有合法的更新对象 - 用 pre-commit hook + gitleaks / git-secrets 阻止
.env进 git - 切 worktree 或 sub-directory 时先确认
.env在哪条路径下,避免多份