1. Prevent — pre-commit hooks
If your repo doesn't run something on git commit, keys will ship. These three tools cover 99% of real leaks.
pre-commit + gitleaks (recommended)
One config file, runs locally and in CI. Fast enough you won't disable it.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
# install once:
# pipx install pre-commit
# pre-commit install
# every `git commit` now blocks on gitleaks findings
trufflehog (deep scan)
Run on PRs. Verifies keys against live APIs so you get a signal not a pile of regexes.
# .github/workflows/trufflehog.yml
name: secret-scan
on: [pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified --fail
keyhound (this project)
Scans public GitHub surface for your org on a schedule. Catches what slips past pre-commit.
# one-shot scan against your org keyhound gh your-org-name --format=json # continuous hourly monitoring (see monitor/ in the repo) cron: '13 * * * *' # runs hourly, diffs, emails only NEW live keys
⚡ Ship pre-commit AND a CI check. Pre-commit catches the dev, CI catches the dev who disabled pre-commit.
2. Detect — find leaks that already shipped
The keys are already public. Finding them fast is most of the fix.
Scan your entire git history
# gitleaks — full history, every branch gitleaks detect --source . --log-opts="--all" # trufflehog — full history, verify live trufflehog git file://. --only-verified # keyhound — public GitHub surface (search, not clone) keyhound gh your-org-name # detect-secrets — scan + baseline for noisy repos detect-secrets scan > .secrets.baseline
GitHub advanced search (manual, works on any repo)
# Swap "acmecorp" for your org/user org:acmecorp "sk-ant-api03-" org:acmecorp "AKIA" "AWS_SECRET_ACCESS_KEY" org:acmecorp filename:.env org:acmecorp "xoxb-" OR "xoxp-" org:acmecorp filename:service-account.json
Check if your key was in a public leak
# GitHub's secret-scanning alerts (if you enabled it) https://github.com/your-org/your-repo/security/secret-scanning # search your repo's PRs + issues — keys often leak in diffs and logs gh pr list --state all --search "sk-" --limit 100 gh issue list --state all --search "AKIA" --limit 100 # search Gists for keys with your org string gh api "search/code?q=sk-ant-api03-+user:your-org" --jq '.items[].html_url'
3. Rotate — provider-by-provider
Revoke first. Rotate second. Audit third. Until the old key is invalidated at the provider, nothing else you do matters.
Anthropic
console.anthropic.com/settings/keysDelete the key. Check the Usage tab for calls you didn't make.
AWS
IAM → Users → the user → Security credentials → Make inactive, then Delete. Review CloudTrail for the exposure window.
GCP
Console → APIs & Services → Credentials. Disable first, then delete. Check Logs Explorer for the key ID.
GitHub PAT
github.com/settings/tokensDelete. If it had push rights, check the Audit log for pushes you didn't make.
Stripe
dashboard.stripe.com/apikeysRoll the key. Review Events + Logs. If you see anomalies, contact Stripe support — they can help reverse.
Slack bot token
App config → OAuth & Permissions → Revoke Token → re-install to regenerate. Review Audit logs if you're on Enterprise Grid.
SendGrid / Twilio
Delete key, rotate. For Twilio, also rotate the Auth Token at the account level — not just the API key.
⚠ Deactivation stops new authentication — the key stops working. What it doesn't do is remove the key from your inventory, which means a month from now an auditor will see it sitting there and won't immediately know whether it's the dead-and-rotated one or a live one someone forgot about. Delete once rotation is verified.
4. Scrub — remove the key from git history
Rotation kills the key. Scrubbing removes it from the commit graph so it stops showing up in scanners, forks, CI caches, Docker layers, and GitHub's permanent commit URLs. A rotated key still in history is a loud "we leaked once" flag that confuses future audits and keeps landing in other people's dashboards.
git-filter-repo (preferred, modern)
# Install: pipx install git-filter-repo # (make a backup clone first — this rewrites history) git clone --mirror git@github.com:your-org/your-repo.git repo-backup cd your-repo # 1) Remove the file entirely from history git filter-repo --invert-paths --path config/.env --path secrets.json # 2) Or: redact a specific string everywhere it appears echo 'sk-ant-api03-XXXXXXXXXXXXXXXXXXXXXXXX==><REDACTED>' > replacements.txt git filter-repo --replace-text replacements.txt # 3) Force-push the rewritten history git push origin --force --all git push origin --force --tags
BFG Repo-Cleaner (faster on huge repos)
java -jar bfg.jar --delete-files .env my-repo.git java -jar bfg.jar --replace-text secrets.txt my-repo.git cd my-repo.git git reflog expire --expire=now --all git gc --prune=now --aggressive git push --force
⚡ Force-push isn't the end. GitHub keeps dangling commits reachable by SHA for a while — anyone who knows the hash can still pull the patch from github.com/<org>/<repo>/commit/<sha>.patch. Open a support ticket asking them to purge the cached refs, and assume any fork you can't control still has the key.
5. Incident checklist — the twenty-minute version
Print this. Follow it top-to-bottom when a leak lands in your inbox.
// did this save you time?
Send the next leak our way
If you spot a leak — yours or someone else's — we route it through a signed, dated, RFC-9116-compliant disclosure. No ransom, no drama.