Claude Code 误把密钥 commit 进 git:6 个来源 + 「先 rotate 后清史」处置

`.env` 或 API key 被 `git add .` 卷进 commit——先 rotate(永远),再清 git 史,最后预防:禁广 `git add` + 装 secret scanner + 收紧 gitignore。

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.productionsecrets.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 #排查 #排查 #安全 #密钥