How to Find and Fix Leaked Secrets and API Keys
Leaked credentials are one of the top causes of breaches, and most leaks are accidental — a key pasted into a commit, a token printed to a log, a password left in a config file. The good news: detection is cheap and the fix is mechanical once you know the steps. This guide shows you how to recognize the common secret formats, scan code and full git history with tools like gitleaks and truffleHog, and run a correct incident response when a real key leaks — the order matters more than people think. Paste a snippet or log into the secret & PII scanner to check it locally, client-side, without uploading anything.
Why Leaked Secrets Matter
Leaked credentials sit at the top of nearly every breach report year after year. The reason is simple: a valid key is a skeleton key. An attacker who finds your AWS secret or your database password doesn't need to break anything — they just log in. And most leaks aren't sophisticated attacks. They're accidents: a key hardcoded into a commit during debugging, a token printed into a log file, a config file checked in "temporarily," a credential pasted into a support ticket or a chat.
The asymmetry is what makes this worth your attention. A leaked key can cost real money (crypto-mining on your cloud account), real data (a dumped database), and real trust. But detecting leaks is cheap — a scanner runs in seconds, and a pre-commit hook costs nothing per commit. The whole discipline is about making the cheap thing automatic so the expensive thing never happens.
Recognize the Formats
Secrets are easier to find than you'd think because most providers stamp their keys with a recognizable prefix. Knowing these means you can grep your own codebase in seconds and you can spot a leak in a log or a screenshot instantly:
- AWS access key ID:
AKIAfollowed by 16 uppercase/digit chars (20 total). The matching secret access key is a 40-character base64-style string with no prefix — much harder to catch, so this is exactly where scanners earn their keep. - GitHub tokens:
ghp_(personal),gho_(OAuth),ghs_(server-to-server), andgithub_pat_for fine-grained personal access tokens. - OpenAI:
sk-and the newersk-proj-. - Anthropic:
sk-ant-. - Google API keys:
AIzafollowed by 35 chars. - Slack:
xoxb-,xoxa-,xoxp-,xoxr-,xoxs-(bot, app, user, refresh, session). - Stripe:
sk_live_(secret) andpk_live_(publishable). The_test_variants are lower-risk but still worth flagging. - JWTs: three base64url segments joined by dots, almost always starting
eyJ(that's{"base64-encoded). See the JWT decoder to inspect one. - PEM private keys: a block starting
-----BEGIN RSA PRIVATE KEY-----or-----BEGIN PRIVATE KEY-----or-----BEGIN OPENSSH PRIVATE KEY-----. - Generic assignments:
API_KEY=,SECRET=,password=,token=next to a long opaque value — no provider prefix, but the variable name is the tell.
A quick local sweep with git grep catches the obvious cases:
git grep -nE 'AKIA[0-9A-Z]{16}|gh[posr]_[0-9A-Za-z]{36}|sk-(ant-|proj-)?[0-9A-Za-z]|AIza[0-9A-Za-z_-]{35}|-----BEGIN [A-Z ]*PRIVATE KEY-----'
That's a blunt instrument — it only scans the current checkout and produces false positives. Dedicated scanners (below) do the same matching across all of history with far better rules.
How to Scan
Quick check: a single snippet or log
When you just want to eyeball a snippet, a log line, or a config blob before pasting it somewhere, use the Janeer secret & PII scanner. It runs entirely in your browser — nothing is uploaded — so it's safe to use with sensitive enterprise data. It flags the common provider key formats plus generic high-entropy strings and PII.
Scan a whole repository (working tree and history)
The critical feature here is scanning git history, not just the files on disk. A secret you removed last month is still sitting in an old commit. Two tools dominate:
# gitleaks — fast, rule-based, scans full history by default
gitleaks detect --source . --redact
# scan only what's staged, before you commit
gitleaks protect --staged --redact
# trufflehog — also verifies findings against live providers
trufflehog git file://. --only-verified
The --only-verified flag is what makes truffleHog special: for many providers it actually attempts to use the credential it found, so it tells you not just "this looks like a key" but "this key is live right now." That cuts through the noise of expired or fake values.
Let your git host catch it: GitHub secret scanning
GitHub automatically scans pushes for known provider token formats. With push protection enabled, it blocks the push outright when it detects one, before the secret ever reaches the server:
remote: error: GH013: Repository rule violations found
remote: - Push cannot contain secrets
remote: —— OpenAI API Key ————————————————————
remote: locations:
remote: - commit: a1b2c3d
remote: path: src/config.js:12
GitHub also notifies partner providers (AWS, Stripe, OpenAI, and many others) when one of their tokens turns up in a public repo, and some providers auto-revoke on receipt. Turn on secret scanning and push protection in repository settings — it's free for public repos and included with GitHub Advanced Security for private ones.
Stop it at the source: pre-commit hooks
The earliest and cheapest place to catch a secret is before it's ever committed. The pre-commit framework wires a scanner into your local commit step:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
# install once per clone
pip install pre-commit
pre-commit install # adds the git hook
# now every `git commit` runs the scanners on staged changes
Yelp's detect-secrets uses a baseline file so it can ignore already-known, already-reviewed findings and only fail on new ones — handy when adopting it on an existing codebase.
The Critical Point: It's in History
This is the single most-misunderstood part of secret leaks, so it's worth stating plainly: removing the secret from the current file is not enough if it was ever committed.
Once a secret has been committed and pushed, it has propagated far beyond the one file you're looking at:
- Git history — every old commit still contains the value;
git log -por a checkout of any earlier commit reveals it. - Clones and forks — every developer who cloned, and every fork, has its own complete copy of that history.
- CI/CD logs — build logs that echoed the value, plus any caches.
- Host-side caches — GitHub may keep a leaked commit visible in cached views even after you rewrite history.
You cannot reach all of those by editing a file. The only action that neutralizes every copy at once is revoking the key — which is why incident response starts there.
Incident Response: When a Real Key Leaks
You found a live credential in your code or history. Work the steps in this order — the order is the whole point.
- Rotate first. Go to the provider, revoke the leaked credential, and issue a new one. Update your apps and secrets manager with the new value. This is the only step that actually stops unauthorized use — do it before cleanup, before notifications, before anything. A revoked key is harmless no matter how many copies exist.
- Check the audit logs. Look at the provider's access/audit logs for the exposure window — from when the secret was first committed to when you rotated. Watch for API calls from unfamiliar IPs, regions, or at odd hours, new resources created, or unusual data access. If you see unauthorized use, escalate to a full incident.
- Scrub it from git history. Now clean the repository so the dead value isn't sitting around.
git filter-repois the recommended tool (BFG Repo-Cleaner is a popular alternative for large repos):
After force-pushing, every collaborator must re-clone or hard-reset — their old clones still contain the secret. Note that GitHub may keep the leaked commit visible in cached views and that forks are untouched, which is exactly why rotation in step 1 is non-negotiable.# git-filter-repo (recommended) pip install git-filter-repo git filter-repo --replace-text <(echo 'AKIAIOSFODNN7EXAMPLE==>REDACTED') # or BFG Repo-Cleaner bfg --replace-text passwords.txt # file lists secrets to scrub git reflog expire --expire=now --all && git gc --prune=now --aggressive # then force-push the rewritten history git push --force --all git push --force --tags - Invalidate anything derived. If the leaked secret was used to mint other credentials — active sessions, downstream service tokens, signed cookies, cached OAuth tokens — invalidate those too. A rotated signing key means little if old tokens signed with it are still honored.
Prevention
Cleanup is the failure mode. The goal is for secrets to never enter the repository in the first place:
- Never commit secrets. Load them from environment variables or a
.envfile, and add.envto.gitignore. Commit a.env.examplethat contains only placeholders (API_KEY=your-key-here), never real values. - Use a secrets manager for shared and production secrets — HashiCorp Vault, AWS Secrets Manager, Doppler, or 1Password — instead of files on disk. Apps fetch secrets at runtime; nothing sensitive is checked in.
- Least privilege + short-lived credentials. Scope each key to exactly what it needs, and prefer short-lived, auto-rotating credentials (IAM roles, OIDC tokens, temporary STS credentials). A leak of a narrow, short-lived key has limited blast radius and a short shelf life.
- Backstops. Run a pre-commit hook (gitleaks or detect-secrets) so a slip is caught locally, and enable GitHub push protection so the host blocks anything that gets past you. Layer them — local hooks can be skipped with
--no-verify, push protection can't.
Common Mistakes
- Committing
.env. The single most common leak. Add it to.gitignoreon day one, before the first commit, and only ever commit.env.examplewith placeholders. - Printing secrets in logs and error messages. Logging a full request or a config object dumps tokens straight into your log aggregator — a second copy outside the repo that's easy to forget. Redact before logging.
- Pasting secrets into chatbots or random online tools. Anything you paste into a server-side tool leaves your control. That's why the Janeer scanner runs client-side — use tools that don't upload your data.
- "Just deleting the line" without rotating. Covered above — the value lives on in history, forks, and caches. Always rotate.
- Hardcoding keys in client-side code. Anything shipped to the browser or a mobile app is public by definition — minification is not security. A "secret" in front-end JavaScript can be read by anyone who opens DevTools. Keep secret keys on the server and proxy the calls.
Try It Live
Paste a snippet, log line, or config blob into the secret & PII scanner to flag leaked keys and personal data — it runs entirely in your browser, so it's safe for sensitive code. Pair it with the JWT decoder to inspect any leaked tokens you find, and the hash generator when you need to hash values you legitimately do have to store.
Frequently Asked Questions
What should I do first when an API key leaks?
Rotate it first. Revoke the leaked credential at the provider and issue a new one before you do anything else — before scrubbing git history, before notifying anyone, before cleanup. Rotation is the only step that actually stops the bleeding: once the key is revoked, it doesn't matter who has a copy of the old value in a fork, a clone, a cached view, or a CI log. Cleaning the secret out of your repository is important for hygiene, but it does nothing to protect you while the key is still valid. Treat any secret that was ever committed and pushed as compromised and rotate it, even if you're not sure anyone saw it.
Is it enough to delete the secret from the file?
No. Deleting the line and committing the change removes the secret from the current version of the file, but the old value still lives in git history — anyone can run git log -p or check out an earlier commit and read it. Worse, once pushed, the secret has also spread to every clone and fork of the repository, to CI build logs, to caches at your git host, and possibly to backups. That's why rotation comes first: it's the only action that neutralizes all those copies at once. Scrubbing history (with git filter-repo or BFG) is still worth doing afterward, but it cannot reach forks or caches, so it is never a substitute for revoking the key.
How do I scan my repository for secrets?
Use a dedicated scanner that reads the full git history, not just the working tree. gitleaks detect --source . --redact walks every commit and matches known secret patterns. trufflehog git file://. --only-verified goes further and actually tries the credentials it finds against live providers to filter out false positives. Run one of these in CI on every push. For a single snippet or log line you want to check by hand, paste it into the Janeer secret scanner, which runs entirely in your browser. And turn on GitHub secret scanning with push protection so the host blocks commits containing recognized provider tokens before they ever land.
What do leaked API keys look like?
Most providers use recognizable prefixes you can grep for. AWS access key IDs start with AKIA and are 20 characters; the matching secret is a 40-character base64-ish string with no prefix, which is harder to spot. GitHub tokens use ghp_, gho_, ghs_, or github_pat_ for fine-grained ones. OpenAI keys start with sk- or sk-proj-, Anthropic with sk-ant-, Google API keys with AIza, Slack tokens with xoxb-/xoxp-/etc., and Stripe live keys with sk_live_ or pk_live_. JWTs are three base64url segments joined by dots, starting eyJ. PEM private keys begin with -----BEGIN ... PRIVATE KEY-----. Generic API_KEY= and password= assignments are also worth flagging.
How do I stop secrets from being committed in the first place?
Keep secrets out of code entirely — load them from environment variables or a .env file that is listed in .gitignore, and commit only a .env.example containing placeholder values. For shared and production secrets, use a secrets manager (HashiCorp Vault, AWS Secrets Manager, Doppler, or 1Password) rather than files. Add backstops so a mistake can't slip through: a pre-commit hook (the pre-commit framework with a gitleaks hook, or detect-secrets) scans staged changes before each commit, and GitHub push protection blocks pushes that contain recognized tokens. Finally, scope credentials with least privilege and prefer short-lived, rotating ones so a leak has limited blast radius and a short shelf life.