Claude Code Accidentally Committed a Secret

`.env` or API key swept into a commit by `git add .`. Rotate first (always), then prevent: ban broad `git add`, install secret scanner, gitignore.

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.example has placeholders only
  • No real keys in tests/fixtures/docs — use obvious fakes like sk_test_FAKE_xxx
  • Quarterly: audit git log -S for old leaked secrets; rotate any still in history
  • For pushed leaks: rotate first, history-rewrite second, never the other way around

Tags: #Claude Code #Debug #Troubleshooting #Security #Secrets