Claude Code 跑了 git add . && git commit -m "...",diff 里包含 .env.local——你的 Stripe live key 在里面。或者它把上次调试用的真 OpenAI key 加进了测试 fixture。commit 还在本地——但只要推了,密钥就上了远端 git 史、GitHub 搜索、所有 fork。
正确反应顺序:先 rotate(永远)、再清 git 史、最后预防。Agent 没法可靠区分「密钥」和「测试数据」——唯一安全规则是永不使用宽 git add + 配 secret scanner 兜底。下面是事件处置 + 四层防御。
常见原因
按命中率从高到低:
1. Agent 用了 git add . 或 git add -A
改完执行 catch-all add。.env.local、.env.production、secrets.json、SSH key——只要不在 .gitignore 里都被扫进来。
如何判断:看 commit 的文件列表——任务范围外的文件出现就是宽 add。
2. .gitignore 没覆盖那个路径
.env 已 ignore 但项目还读 config/secrets.json 或 .env.development.local——要么 gitignore 过期,要么从没盖到这条路径。
如何判断:git check-ignore -v <secret-path> 没返回——文件没被 ignore。
3. 测试 fixture / doc 里硬编了密钥
.test.ts 里写了真 key 来调试”一会儿”。Agent 看作合法测试数据 commit 了。
如何判断:密钥出现在和 config 无关的文件——测试、doc、script——来源是「调试遗留」。
4. .env.example 里写了真值不是占位
加新变量时 STRIPE_LIVE_KEY= 文档化到 .env.example,结果打了真 key 而不是 <your-key-here>。Agent commit .env.example(这个文件不在 gitignore)。
如何判断:.env.example 里有真值样的字符串而不是占位符。
5. token 进了 commit message 或分支名
分支叫 fix/sk-test-abc123-payment-bug(字面 key);或 commit message 里有 // debug: sk_live_xxx——diff 里没但 git metadata 漏了。
如何判断:在 history 里搜 sk_、ghp_、xoxb_ 这类前缀,包括 commit message。
6. 生成产物里嵌了密钥
构建输出、source map、编译 bundle 内联了 secret。Agent 还 commit 了 dist/(本来就不该 commit)。
如何判断:密钥在生成文件里。dist/、build/、out/、coverage/ 根本不该进 git。
最短修复路径
按紧迫度排序。已推的情况下 Step 1 和 2 必须 5 分钟内做完。
Step 1:立刻 rotate 密钥
假定已泄漏。任何 push 出去的密钥都当公开——立刻轮换:
Stripe → API keys → roll 受影响的 key
OpenAI → API keys → revoke + 新建
AWS → IAM → 停用 key + 新建
GitHub PAT → Settings → Tokens → revoke + 新建
更新 .env.local 和部署平台的环境变量,验证生产用新 key 还能跑。
「我推之前 catch 到了」不要省这一步——文件曾经在磁盘上,如果机器被备份过或任何工具索引过 workspace,可能已经溜出去了。
Step 2:从 git 史里删(若已推)
未推的:
# 简单情况:还没推,密钥只在 HEAD
git reset HEAD~1
# 改/删文件里的密钥
git add <other-files-only>
git commit -m "..."
已推的需要重写 history:
# 用 git-filter-repo(推荐,比 filter-branch 好)
pip install git-filter-repo
# 删整个文件的所有历史
git filter-repo --invert-paths --path .env.local
# 或在所有 blob 内容里替换特定字符串
echo "sk_live_REAL_KEY_HERE" > /tmp/secret-replacements.txt
git filter-repo --replace-text /tmp/secret-replacements.txt
# 强推(要和团队协调——history 被改写)
git push --force-with-lease origin main
强推后每个协作者得重 clone 或 rebase。有 fork 或本地 clone 的人可能还有密钥。
repo 公开的话密钥本就公开了——rotate 才是真重要。重写 history 只减少随手发现的概率,并不能撤销已发生的暴露。
Step 3:CLAUDE.md 禁宽 git add
## Git 政策
永远不要用:
- `git add .`
- `git add -A`
- `git add --all`
始终用:
- 显式路径:`git add src/billing.ts src/billing.test.ts`
- 或交互式:`git add -p`(逐 hunk 接受)
每次 `git add` 前 `git status` 一遍,确认只有预期文件。
单条最有效——没有宽 add,密钥就没法被卷进来。
Step 4:装 pre-commit secret scanner
选一个:
# git-secrets(AWS 风格模式)
brew install git-secrets
git secrets --install
git secrets --register-aws
# detect-secrets(Python,可配置性强)
pip install detect-secrets
detect-secrets scan > .secrets.baseline
或走 pre-commit:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
每个 commit(agent 的、你的)创建前都扫一遍。
Step 5:收紧 .gitignore + .env.example 检查
# .gitignore
.env
.env.local
.env.*.local
.env.production
secrets/
*.pem
*.key
*.p12
config/secrets.json
CI 加一条:.env.example 不能有真值:
# scripts/check-env-example.sh
if grep -E 'sk_(live|test)_[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{36}' .env.example; then
echo ".env.example 里有像真密钥的字符串"
exit 1
fi
Step 6:CI 加 secret 扫描兜底
pre-commit 只对本地装了的人生效——CI 始终运行:
# .github/workflows/secret-scan.yml
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GitHub 自带的 secret scanning 也覆盖各家 provider 的已知模式——repo Settings → Code security 里开。
预防建议
- CLAUDE.md 写死:永不用
git add .或-A,始终显式路径或-p - pre-commit secret scanner(detect-secrets / git-secrets / gitleaks)——本地 + CI 双层
- 密钥只放
.env.local、gitignore;.env.example只放占位符 - 测试 / fixture / doc 里不放真 key——用明显的假值
sk_test_FAKE_xxx - 季度审计
git log -S找旧泄漏密钥——还在 history 里的全 rotate - 已推泄漏的处置顺序:先 rotate、再重写 history——绝不反过来
相关阅读
标签: #Claude Code #排查 #排查 #安全 #密钥