你打开 .env 准备跑项目,发现里面所有 API key 都变成了 your_api_key_here、xxx、replace_me。在某轮”清理项目”或”补全 .env 模板”时,Claude Code、Cursor 或 Aider 把 .env 当成模板,把你真实的密钥全擦了。更糟的情况:它把 .env 直接删了,git 也没追踪,本地没有备份。
最快的恢复方式(先做这个):在 VS Code 或 Cursor 里,在资源管理器里右键 .env,选 Open Timeline,点中 agent 改动之前的那条记录,再选 Restore Contents。Local History 默认开启(截至 2026 年 6 月 workbench.localHistory.enabled 默认为 true),会悄悄给每次保存打快照,所以它是最可靠的恢复来源。如果 agent 是通过终端而不是编辑器写的这个文件,看下面完整的恢复表。
这篇先讲怎么恢复,再讲怎么彻底锁死让它别再碰。光靠 agent ignore 文件是不够的——有已知案例(2026 年初)表明,ignore 规则甚至 permissions.deny 都可能被终端或 MCP 工具调用绕过,所以我们要叠好几层防护。
常见原因
按命中率从高到低排序。
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”,它顺手把整个文件重写了一遍,所有现有值都被替换成默认示例值,因为它判断这样”更整齐”。
如何判断:ls -la .env 看 mtime 是不是刚被改过——git 追踪不到被 ignore 的文件,看不出 diff,但 mtime 能确认。
3. 编辑器扩展或 formatter 保存时重写
VS Code 的某些 dotenv 扩展会在保存时”美化”.env——把值用引号包起来、对齐 =、按字母排序,有些 bug 版本直接把 value 清空。Cursor 的 agent 触发保存时也会触发这些扩展。
如何判断:禁用 dotenv 相关扩展,让 agent 再编辑一次,看值是否还会被改。
4. Agent 把 .env 和 .env.example 写反了
它本来要更新 .env.example(提交的模板),结果路径写错,把 .env(你的真值)覆盖成了模板内容。
如何判断:看 agent 最近的 Write / Edit 工具调用,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. 多 worktree / 多机器混淆
Agent 在 git worktree 或 sub-directory 里干活,创建了一个新的 .env(空模板),你切回去时 IDE 看到的是它创建的那个。
如何判断:find . -name ".env*" -not -path "./node_modules/*",看是否有多个 .env。
你属于哪种情况
| 症状 | 最可能的原因 | 怎么办 |
|---|---|---|
所有值变成 your_..._here / xxx | 原因 1 或 2 | 先恢复再锁死 |
| 值被加引号 / 重新对齐 / 排序,但还是真值 | 原因 3(formatter) | 禁用扩展 |
.env 整个不见了 | 原因 4 或 5 | Timeline / 备份 |
出现两个 .env,其中一个是空的 | 原因 6(worktree) | find 找出真的那个 |
最短修复路径
Step 1 是抢救。Step 2-6 防止再次发生,能叠几层叠几层。
Step 1:恢复真实值
按”成功率从高到低”试这几个来源。
| 来源 | 命令 / 操作 |
|---|---|
| VS Code / Cursor Local History | 右键 .env → Open Timeline → 选改动前的那条 → Restore Contents |
| 部署平台 env 面板 | Vercel/Netlify/Railway:项目 → Settings → Environment Variables → 显示并复制 |
| 1Password / Bitwarden / 公司密码管理器 | 打开之前存的 secret 条目 |
| macOS Time Machine | tmutil restore /Users/you/project/.env |
| Git stash(如果你之前手动 stash 过) | git stash list,再 git stash show -p 'stash@{0}' |
| Shell history | grep -i "API_KEY=" ~/.zsh_history(或 ~/.bash_history)找曾 export 过的 |
| 团队 Slack / Notion | 翻聊天记录里共享过的截图或粘贴 |
Local History(workbench.localHistory.enabled,默认开启)每个文件最多保留 50 条记录、每条上限 256 KB。它唯一的盲区:只对编辑器内部的保存打快照。如果 agent 是通过终端命令(cp、>、setup 脚本)重写 .env,可能根本没有 Timeline 记录——这时回退到部署平台或密码管理器。
Step 2:在 .gitignore 旁边写 agent ignore 文件
不同 agent 用不同文件名,全写上:
# .cursorignore (Cursor——完全屏蔽:不索引、agent 不读、@-mention 也读不到)
.env
.env.local
.env.*.local
*.pem
*.key
# .aiderignore (Aider——用 .gitignore 语法)
.env
.env.*
# .gitignore 也保留
.env
.env.local
截至 2026 年 6 月,有两点要注意:
- Cursor 也会读
.cursorindexingignore,但它只是把文件从代码库搜索里隐藏——你 @-mention 或拖进对话框时,agent 仍然能读。要真正屏蔽请用.cursorignore,不要用.cursorindexingignore。Cursor 默认已经忽略.env,但显式写上更稳妥。 - ignore 文件管的是 Cursor 自己的文件访问;终端命令和 MCP 工具运行在这个沙箱之外,仍然可以读写
.env。所以 Step 3-6 才重要。
Step 3:用 Claude Code 的 permissions.deny 锁死
Claude Code 从 .claude/settings.json 读 permissions。deny 规则比 ignore 文件更强,因为它直接挡住 Read/Write/Edit 工具:
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Write(./.env)",
"Edit(./.env)",
"Read(./secrets/**)"
]
}
}
把它作为 .claude/settings.json 提交,全团队共享(个人专属覆盖用 .claude/settings.local.json;优先级是 managed > local > project > user)。有两个已知限制要预先考虑:
- 有个已报告的缓存 bug:新加的
Read(*/.env*)deny 规则有时要等到本轮会话里再次编辑.claude/settings.json后才生效——如果漏了一次,把 settings 文件再保存一遍并重启会话。 deny挡的是 Claude Code 自己的文件工具,挡不住 Bash 工具里敲的cat .env。想做到滴水不漏,再加一条对应的 Bash deny,比如"Bash(cat ./.env*)"。
Step 4:在 CLAUDE.md / AGENTS.md / .cursorrules 里写死规则
## 文件保护
- `.env` 和 `.env.*`(除了 `.env.example`)严禁读取、编辑、删除、覆盖
- 需要新增环境变量时,只能更新 `.env.example`,并在 PR 描述里列出新增的 key
- 任何"清理项目"、"标准化配置"、"补全 .env"类操作都必须先问用户
- 用户的真实密钥永远不应该出现在 agent 的输出里
把这段直接复制进项目根的 instruction 文件。Claude Code、Cursor、Aider、Codex 每轮都会读这些文件——但它是软约束(模型仍可能无视),所以当作其中一层,不是全部防线。
Step 5:提交一份”安全”的 .env.example
让 agent 有合法目标可写,不去碰真 .env:
# 从真 .env 生成模板(值清掉)
sed -E 's/=.*/=/' .env > .env.example
git add .env.example
git commit -m "chore: add .env.example template"
之后”加新变量”的请求更新的就是 .env.example,你再手动同步到本地 .env。
Step 6:加 pre-commit hook 防误提
哪怕 agent 真把 .env 提交了,hook 也拦下来:
# .git/hooks/pre-commit (记得 chmod +x;或用 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
(网上常见的一行写法 grep -q ... | grep -v 是错的:grep -q 什么都不输出,后面那个 grep 永远匹配不到。改成像上面这样把匹配结果存进变量。)
想更稳妥,加 gitleaks,这样即便文件改了名或密钥是直接粘进代码的也拦得住:
# 安装:brew install gitleaks
# 写进同一个 pre-commit hook:
gitleaks protect --staged --redact || exit 1
gitleaks 扫描暂存区里的 secret 模式(sk_live_*、AKIA*、-----BEGIN PRIVATE KEY-----),不管文件叫什么名字。如果加不了 gitleaks,git-secrets 是更老的替代品。
怎么确认修好了
cat .env又显示出你的真实值(不再是your_..._here)。- 应用能启动并通过鉴权:
npm run dev(或你的启动命令)不再报 “missing/invalid API key”。 - 随手开一轮让 agent “整理一下项目配置”——它现在应该拒绝读或重写
.env。在 Claude Code 里你会看到.env上的 permission-denied 提示;在 Cursor 里这个文件不会进它的上下文。 - 故意 stage 一下
.env再尝试 commit——pre-commit hook 必须拦住。
预防建议
- 在
.gitignore和每种 agent 的 ignore 文件(.cursorignore、.aiderignore)里都列出.env*。 - 在
.claude/settings.json里为.env加permissions.deny(再加一条对应的 Bash deny)。 - 在 CLAUDE.md / AGENTS.md /
.cursorrules里写明”严禁触碰 .env”,列出具体路径。 - 真实密钥存在密码管理器(1Password / Bitwarden)或部署平台的 env 面板,而不是只存在本地
.env。 - 定期把
.env备份到加密位置:私有 repo 里一个age加密的文件,或公司 secret manager。 - 提交
.env.example作为模板,让 agent 有合法的更新对象。 - 用 pre-commit hook + gitleaks 阻止
.env和裸密钥进 git。 - 切 worktree 或 sub-directory 时先确认在改哪个
.env,避免多份并存。
常见问题
agent 把 .env 删了、又从来没进过 git,还能找回来吗?
通常可以。先看编辑器 Timeline(Local History 连被 gitignore 的文件也会打快照),再看部署平台的 env 面板,再看密码管理器。三处都空的话,就到各家服务商后台重新生成 key——绝不要复用一个无法确认完整的密钥。
为什么把 .env 加进 .gitignore 反而更容易出事?
因为 agent 通过 git 读不到被 ignore 文件的内容,它只看得到塞满占位符的 .env.example,于是”好心”地补全缺口。出于安全 .env 该被 gitignore,但要同时配上 ignore 文件和 permissions.deny,让 agent 也写不进去。
我给 .env 设的 permissions.deny 不生效——是 bug 吗?
有可能。有个已知缓存问题:新加的 Read(*/.env*) deny 规则要等到同一会话里再次编辑 .claude/settings.json 后才生效。把 settings 文件重新保存一遍并重启会话。另外记住 deny 只挡 Claude Code 的 Read/Write/Edit 工具——Bash 工具里的 cat .env 会漏过去,除非你再加一条 Bash deny。
该用 .cursorignore 还是 .cursorindexingignore?
密钥用 .cursorignore。.cursorindexingignore 只是把文件从代码库搜索里去掉——agent 仍能通过 @-mention 读到。两者都挡不住 Cursor 的终端或 MCP 工具,所以部署平台 / 密码管理器的备份才是真正的兜底。
怎么既挡住这事、又不影响 agent 正常干活?
只屏蔽密钥文件(.env、*.pem、*.key、secrets/**),别把整棵配置树都锁了。提交一份 .env.example,让 agent 仍有合法地方加新 key,并在 CLAUDE.md 里告诉它更新示例文件、在 PR 里列出新增的 key。