Claude Code ran git add . && git commit -m "..." and the diff includes .env.local with your live Stripe key. Or it added a test fixture containing a real OpenAI API key from your last debugging session. The commit is local — but if it was pushed, that secret is now in remote git history, GitHub search, and any forks.
The right reaction is: rotate first (always), purge from history second, prevent third. Agents cannot reliably distinguish “secret” from “test data”; the only safe rule is never use broad git add patterns and have a secret scanner as a backstop. Below: how to handle the incident now, and the four-layer defense that stops the next one.
Common causes
Ordered by hit rate, highest first.
1. Agent used git add . or git add -A
The agent finished editing and ran the catch-all add. .env.local, .env.production, secrets.json, an SSH key — all swept in if they weren’t in .gitignore.
How to spot it: Check the commit’s file list. If any file outside the task scope is included, the staging was broad.
2. .gitignore doesn’t cover the secret path
You have .env ignored but your project also reads config/secrets.json or .env.development.local. Either the gitignore is stale or never covered this path.
How to spot it: git check-ignore -v <secret-path> returns nothing — meaning the file is not ignored.
3. Secret hardcoded in test fixture / docs
A .test.ts file contains a real key for debugging “just for a minute.” Agent picked it up as legitimate test data and committed.
How to spot it: Secret appears in a file that has nothing to do with config — a test, a doc, a script. Provenance is “left over from debugging.”
4. New env variable in .env but .env.example got the real value
You added STRIPE_LIVE_KEY= to .env.example to document the new var, but accidentally typed the real key instead of <your-key-here>. Agent committed .env.example (not gitignored).
How to spot it: .env.example has real-looking values, not placeholders.
5. Token in commit message or branch name
Branch named fix/sk-test-abc123-payment-bug (literal key). Or commit message includes // debug: sk_live_xxx. Not “in” the diff but still leaked into git metadata.
How to spot it: Search history for sk_, ghp_, xoxb_, or whatever prefix your platform uses, including in commit messages.
6. Generated artifact contains an embedded secret
Build output, source map, or compiled bundle ends up with the secret inlined. Agent committed dist/ (which it shouldn’t have anyway).
How to spot it: Secret is in a generated file. dist/, build/, out/, coverage/ shouldn’t be in git at all.
Shortest path to fix
Ordered by urgency. Steps 1 and 2 must run within 5 minutes if the commit was pushed.
Step 1: Rotate the secret immediately
Assume compromised. Treat any pushed secret as public. Rotate now:
Stripe → API keys → roll the affected key
OpenAI → API keys → revoke + create new
AWS → IAM → deactivate the key + create new
GitHub PAT → Settings → Tokens → revoke + create new
Update your .env.local and deployment env vars with the new value. Test that production still works with the new key.
Do not skip this even if you “caught it before pushing” — the file existed on disk, and if your machine has been backed up or any tool indexed the workspace, it may have escaped.
Step 2: Remove from git history (if pushed)
For unpushed commits:
# Easy case: not pushed yet, secret is in HEAD only
git reset HEAD~1
# Edit/remove the secret from the file
git add <other-files-only>
git commit -m "..."
For pushed commits, history rewrite:
# Use git-filter-repo (recommended over filter-branch)
pip install git-filter-repo
# Strip a specific file from all history
git filter-repo --invert-paths --path .env.local
# Or strip a specific string from all blob content
echo "sk_live_REAL_KEY_HERE" > /tmp/secret-replacements.txt
git filter-repo --replace-text /tmp/secret-replacements.txt
# Force-push (coordinate with team — this rewrites history)
git push --force-with-lease origin main
After force-push: every collaborator must re-clone or rebase. Anyone with a fork or local clone may still have the secret.
If the repo is public, the secret is already public — rotation is what matters. History rewrite reduces casual discovery but doesn’t undo exposure.
Step 3: Ban broad git add in CLAUDE.md
## Git policy
NEVER use:
- `git add .`
- `git add -A`
- `git add --all`
ALWAYS use:
- Explicit file paths: `git add src/billing.ts src/billing.test.ts`
- Or interactive: `git add -p` (then accept individual hunks)
Before EVERY `git add`, run `git status` and verify only intended files appear.
This is the single most effective rule — without broad add, secrets can’t be swept in by accident.
Step 4: Install a pre-commit secret scanner
Choose one:
# git-secrets (AWS-style patterns)
brew install git-secrets
git secrets --install
git secrets --register-aws
# detect-secrets (Python, more configurable)
pip install detect-secrets
detect-secrets scan > .secrets.baseline
Or via 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']
Now every commit (agent’s or yours) is scanned before being created.
Step 5: Harden .gitignore and add a .env.example check
# .gitignore
.env
.env.local
.env.*.local
.env.production
secrets/
*.pem
*.key
*.p12
config/secrets.json
Add CI check that .env.example doesn’t contain real-looking values:
# 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 "Real-looking secret in .env.example"
exit 1
fi
Step 6: Add CI secret-scanning as a backstop
Pre-commit hooks only fire if installed locally. CI runs always:
# .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’s native secret scanning also catches known provider patterns and notifies you — enable it in repo Settings → Code security.
Prevention
- CLAUDE.md rule: never
git add .or-A; always explicit paths or-p - Pre-commit secret scanner (detect-secrets, git-secrets, gitleaks) — local + CI both
- Every secret lives in
.env.local, gitignored;.env.examplehas placeholders only - No real keys in tests/fixtures/docs — use obvious fakes like
sk_test_FAKE_xxx - Quarterly: audit
git log -Sfor old leaked secrets; rotate any still in history - For pushed leaks: rotate first, history-rewrite second, never the other way around
Related
Tags: #Claude Code #Debug #Troubleshooting #Security #Secrets