Find leaked secrets in source code, Git history, build artifacts, and infrastructure — and audit the secrets-management posture preventing future leaks. Use when the user mentions 'secrets audit,' 'secret scanning,' 'leaked credentials,' 'API key in code,' 'gitleaks,' 'trufflehog,' 'git history scan,' 'secrets management,' 'vault audit,' 'rotation policy,' 'AWS Secrets Manager,' 'HashiCorp Vault,' 'Doppler,' '1Password Secrets Automation,' 'sealed-secrets,' 'External Secrets Operator,' 'secrets in MCP config,' 'API key in Claude transcript,' 'is this key public,' or needs to find or prevent credential exposure.
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Two halves: (1) find secrets that have already leaked into source, history, or artifacts, and (2) audit the secrets-management posture that determines whether future leaks happen.
Most secret leaks aren't "we forgot to redact" — they're "we never had a system, so every developer made up their own approach." This skill covers both the cleanup and the prevention.
Cross-references: dependency-audit (CI-related secrets risk in build-time exposure), iam-audit (workload identity federation as the alternative to long-lived keys), owasp-audit A04:2025 Cryptographic Failures (hard-coded secrets found during an app review), agent-supply-chain-audit (the skills, hooks, and MCP servers that sit next to these agent configs and can read them), incident-triage and threat-hunting (the exfiltration-collector domains below as egress indicators), scanner-pipeline (gitleaks / trufflehog across full history and a whole fleet of repos, on a schedule), personal-opsec (the operator's own machine, where agent tokens and pasted secrets actually live), cicd-security (which workflows and forks can reach CI secrets).
Scan only repositories, CI systems and machines the user owns or is authorized to audit. Never print a full secret: give the type, location, and prefix plus last four. Checking that a key is live ("Verify it's live" below) uses the key, so ask first, use the provider's read-only identity call, and never use a found credential to read data. Rotation and history rewrites are the owner's call and always prompt.
The most useful first sweep is grep against known provider key prefixes. False positives are low and matches are almost always real.
These list matching files (-l) or counts (-c) rather than printing the match, so the secret isn't copied into your terminal scrollback or an agent transcript. Use the masked rg below when you need to tell keys apart.
# Stripe
grep -rlE "(sk_live_|sk_test_|rk_live_|whsec_)[A-Za-z0-9]{20,}" . \
--include="*.{js,ts,jsx,tsx,py,rb,go,java,php,sh,env,yml,yaml,json}"
# AWS access keys
grep -rlE "(AKIA|ASIA)[A-Z0-9]{16}" .
# AWS secret keys (40 chars, base64-y). High FP rate: lockfile integrity hashes match too. Counts per file first
grep -rcE "[A-Za-z0-9/+=]{40}" . --include="*.env*" --include="*.json" | grep -v ':0$'
# GitHub
grep -rlE "gh[pousr]_[A-Za-z0-9]{36}" .
# Google Cloud API key + service-account JSON
grep -rlE "AIza[A-Za-z0-9_-]{35}" .
grep -rln '"type": "service_account"' . --include="*.json"
# Slack
grep -rlE "xox[baprs]-[A-Za-z0-9-]+" .
# OpenAI / Anthropic
grep -rlE "sk-[A-Za-z0-9]{32,}" .
grep -rlE "sk-ant-[A-Za-z0-9_-]{90,}" .
# Generic high-entropy values in env files: variable names and line numbers, value masked to first/last 4
grep -rnE "^[A-Z_]+=[A-Za-z0-9/+=]{32,}$" . --include="*.env*" | sed -E 's/=(.{4}).*(.{4})$/=\1…\2/'For full repo coverage, use git ls-files to scope to tracked files and avoid node_modules:
git ls-files | xargs grep -lE 'sk_live_|ghp_|AKIA[A-Z0-9]{16}|sk-ant-|AIza[A-Za-z0-9_-]{35}' 2>/dev/nullRunning these inside an AI agent session? Every match the agent prints is written into that session's transcript
(see the agent section below), which creates a new copy of the secret. List files (-l) or counts (-c), or mask the
value to prefix + last 4 when you need to tell keys apart:
rg -o -r '$1…$2' '(sk_live_|rk_live_|sk-ant-|ghp_|github_pat_|AKIA|xox[baprs]-|AIza)[A-Za-z0-9_-]{8,}([A-Za-z0-9]{4})' .A prefix match tells you the provider, not the authority. Several providers issue keys that are public by design, and the same prefix can cover both. Before rating, establish what kind of key it is and what it can do — the severity follows the authority, not the match (references/severity-calibration.md).
| Provider | Publishable (meant for clients) | Restricted / scoped | Secret (full authority) |
|---|---|---|---|
AIza… browser / Maps / Firebase keys — fine when application restrictions (referrer, app, IP) and API restrictions are set | Same AIza… prefix with API restrictions; check which APIs it can call in Cloud Console → Credentials | An unrestricted AIza… key on a project with billable or data-bearing APIs enabled (Maps, Generative Language / Gemini) acts like a secret; service-account JSON always is | |
| Stripe | pk_live_ / pk_test_ — public, not a finding | rk_live_ — rate by the permissions set on that key in the Dashboard | sk_live_ — full account; sk_test_ is test-mode only (lower); whsec_ lets an attacker forge webhooks, rate by what the handler does |
| Supabase | anon JWT / sb_publishable_… — public; exposure is about RLS, which is an authorization finding (owasp-audit / api-audit), not a secret | — | service_role JWT / sb_secret_… — bypasses RLS entirely |
| Firebase | Web config (apiKey, authDomain, projectId, appId) — identifiers, not secrets; the risk lives in Security Rules and App Check | — | Admin SDK service-account JSON, CI deploy tokens |
| Others | Mapbox pk., Algolia search-only key, Sentry DSN, PostHog phc_ | — | Mapbox sk., Algolia admin key, PostHog phx_ personal key |
Decode a Supabase key's JWT payload locally (the middle segment, base64) to read its role — that doesn't use the key.
A publishable key with correct restrictions is rejected (name the restriction and where it's configured). A
publishable key missing its restrictions is a finding only when you can trace the abuse (billing, quota exhaustion,
data the enabled API returns); otherwise it's a hardening note.
| Tool | Use |
|---|---|
gitleaks detect | Fast, low FP, run as pre-commit and in CI; supports custom rules |
trufflehog git file://. | Verifies findings against the real API (high confidence) |
detect-secrets scan | Yelp's tool; good baseline file workflow |
| GitHub Secret Scanning | Free for public repos; covers most providers automatically; pushes get blocked at push time when enabled with push protection |
| GitLab Secret Detection | Similar, built-in to CI |
| GitGuardian / Doppler / Spectral | Commercial; add organizational dashboards and historical analysis |
A secret deleted in the latest commit is still in history — git log -p, git log -S<secret>, and any fork or local clone all have it.
# Search every commit for a pattern
git log -p -S "sk_live_" --all
# Search only deleted lines
git log -p --all | grep -E "^-.*sk_live_"
# Trufflehog historical scan
trufflehog git file://. --since-commit=<first-commit>
# Git history rewrite — destructive, coordinate first
git filter-repo --invert-paths --path config/secrets.yml
# or
bfg --delete-files secrets.ymlCritical caveat: rewriting history requires every developer to re-clone, every fork is still exposed, and the secret should be considered compromised regardless. Always rotate first, history-rewrite second.
Secrets leak in places that aren't .env files:
docker history <image> shows every ENV line; --build-arg SECRET=... ends up in layersset -x, console.log(process.env), error stack traces, debug outputNEXT_PUBLIC_* / VITE_* / REACT_APP_* env vars are shipped to the browser; grep the bundled JSprocess.env snapshotspg_dump of a table that includes user-stored API keys.env accidentally uploadedAgents need tokens for their MCP servers and helpers, and developers paste keys into chat to "just test it". Both end up in plaintext files that are rarely gitignored deliberately and are often synced or backed up.
| Location | What leaks |
|---|---|
.mcp.json, ~/.claude.json (user and per-project mcpServers) | env and headers blocks with literal tokens |
.cursor/mcp.json, ~/.cursor/mcp.json, .vscode/mcp.json | Same, for Cursor and VS Code / Copilot |
~/Library/Application Support/Claude/claude_desktop_config.json (Windows %APPDATA%\Claude\) | Claude Desktop MCP env blocks |
.claude/settings.json, .claude/settings.local.json, ~/.claude/settings.json | env blocks; apiKeyHelper / awsCredentialExport commands that print a credential (and run a shell command — see agent-supply-chain-audit) |
~/.codex/config.toml, ~/.codex/auth.json | [mcp_servers.<name>.env] tables; the Codex login / API key |
~/.gemini/settings.json, .gemini/settings.json, .gemini/.env, ~/.gemini/.env | mcpServers env; Gemini API keys |
~/.claude/projects/**/*.jsonl (plus ~/.claude/history.jsonl, shell snapshots, file history where present) | Session transcripts: every pasted key, every cat .env, every secret a tool printed |
~/.codex/sessions/**/*.jsonl, ~/.codex/history.jsonl, ~/.gemini/tmp/ | Codex and Gemini session logs and checkpoints |
Cursor / VS Code User/workspaceStorage/*/state.vscdb and chat-session folders | Chat history in SQLite / JSON |
Dotfiles repos and backups (Time Machine, iCloud, a ~/.claude checked into dotfiles) | Copies of all of the above |
# MCP env / headers: any value that isn't a ${VAR} reference is a literal secret candidate
rg -n -A10 '"(env|headers)"\s*:' .mcp.json .cursor/mcp.json .vscode/mcp.json ~/.claude.json ~/.cursor/mcp.json \
~/.gemini/settings.json "$HOME/Library/Application Support/Claude/claude_desktop_config.json" 2>/dev/null \
| rg -o -r '$1$2 = <literal>' '^(\S+?[:-]\d+[:-]).*?"([A-Za-z0-9_]+)"\s*:\s*"[^"$]{12,}"' # key names only, values masked
rg -n '\[mcp_servers\.[^]]+\.env\]|^\s*env\s*=\s*\{' ~/.codex/config.toml # then read the table in an editor, not into the session
# Agent settings: env blocks and credential helpers
rg -n '"(env|apiKeyHelper|awsCredentialExport|awsAuthRefresh|otelHeadersHelper)"\s*:' \
.claude/settings.json .claude/settings.local.json ~/.claude/settings.json
# Transcripts and logs: counts per file only — printing a match copies it into this session's transcript
rg -c -e 'sk-ant-' -e 'sk_live_' -e 'rk_live_' -e 'ghp_' -e 'github_pat_' -e 'AKIA[A-Z0-9]{16}' -e 'xox[baprs]-' \
-e 'AIza[A-Za-z0-9_-]{35}' -e '-----BEGIN [A-Z ]*PRIVATE KEY' -e 'service_role' \
~/.claude/projects ~/.claude/history.jsonl ~/.codex/sessions ~/.gemini/tmp 2>/dev/null
rg -a -c -e 'sk-ant-' -e 'sk_live_' -e 'ghp_' -e 'AKIA[A-Z0-9]{16}' \
"$HOME/Library/Application Support/Cursor/User/workspaceStorage" 2>/dev/null
# Are any of these tracked?
git ls-files | rg '(^|/)(\.mcp\.json|mcp\.json|mcp_config\.json|settings\.local\.json|claude_desktop_config\.json|auth\.json|\.env)$'For a key found in a transcript: rotate it (the transcript is a copy you can't recall from backups), then delete or scrub
the session file. Shorten retention where the agent supports it (Claude Code cleanupPeriodDays).
Malicious skills, hooks, and packages ship stolen secrets to free collectors and tunnels. Seeing one of these in a
config, hook command, skill, bundled script, or package is a strong candidate for exfiltration; seeing one in egress
logs or DNS is an indicator for incident-triage (scope a live incident) and threat-hunting (sweep for it). Some
have legitimate uses — a team Slack webhook in CI notifications — so confirm what is sent.
| Kind | Domains / URL shapes |
|---|---|
| Chat webhooks | discord.com/api/webhooks/, discordapp.com/api/webhooks/, api.telegram.org/bot, hooks.slack.com/services/ (and /workflows/, /triggers/) |
| Request collectors | webhook.site, requestbin / *.m.pipedream.net, requestcatcher.com, beeceptor.com, pipedream.net |
| OAST / interaction | interact.sh, *.oast.pro / .live / .site / .online / .fun / .me, burpcollaborator.net, oastify.com |
| Tunnels | *.ngrok.io, *.ngrok-free.app, *.ngrok.app, *.trycloudflare.com, loca.lt, serveo.net |
| Paste / drop | pastebin.com, transfer.sh, paste.ee, 0x0.st |
rg -n -i 'discord(app)?\.com/api/webhooks|api\.telegram\.org/bot|hooks\.slack\.com/(services|workflows|triggers)|webhook\.site|requestbin|pipedream\.net|requestcatcher\.com|beeceptor\.com|interact\.sh|oast\.(pro|live|site|online|fun|me)|burpcollaborator\.net|oastify\.com|ngrok(-free)?\.(io|app|dev)|trycloudflare\.com|loca\.lt|serveo\.net|pastebin\.com|transfer\.sh|paste\.ee|0x0\.st' .A Slack or Discord webhook URL is itself a secret (anyone holding it can post to that channel) — report it under both headings.
When you find a leaked secret:
aws sts get-caller-identity, stripe balance retrieve, curl -H "Authorization: Bearer $TOKEN" ...) — don't assume; some leaked keys are already revoked or were sandbox-onlyrepo vs admin:org| Tier | Pattern | When acceptable |
|---|---|---|
| ❌ | Hardcoded in source | Never |
| ❌ | Hardcoded in image / build artifact | Never |
| ❌ | Plaintext in shared docs / Slack | Never |
| ⚠️ | .env file in repo (even with .gitignore — easy to leak via push, backup, archive) | Bootstrap only; flagged in audit |
| ⚠️ | Environment variables (only) | Acceptable for ephemeral dev; weak for prod (visible in /proc, crash dumps, logs) |
| 🟢 | Secrets manager pulled at deploy time | Standard for most apps |
| 🟢 | Workload identity federation (no stored secret at all) | Best where supported |
.env committed — .gitignore covers .env* (with care for .env.example).gitignore, or ${ENV} references only — .mcp.json, .cursor/mcp.json, .vscode/mcp.json, .claude/settings.local.json, .gemini/.env are either untracked (git check-ignore -v <file>) or hold only variable references (${VAR} in Claude Code .mcp.json, ${env:VAR} in Cursor, ${input:…} prompts in VS Code); agent transcript retention is set and transcripts aren't in dotfile repos or unencrypted backupsiam-audit).env.local shipped to staging — environment-specific dev secrets cross the boundarypull_request_target and secret accessibility → cicd-security (fork secrets, pwn requests)--build-arg AWS_SECRET=... ends up in image history (use --secret/BuildKit instead)process.env on unhandled exception — Sentry / Datadog / Bugsnag scrub config requiredcontainer-audit)Write findings.json first, validate it, then render the markdown report from it — the prose never changes a verdict
or severity. Record shape and the validate / SARIF-export commands are in
references/findings-format.md; run
node references/validate-findings.mjs findings.json (references/validate-findings.mjs) and fix every error before writing prose.
findings.json (or the report). Redact to prefix + last 4 — sk_live_…9f2c — in
titles and evidence descriptions.secrets-audit/<provider>-<key type>/<file>/<variable>, the form in findings-format.md, e.g.
secrets-audit/stripe-secret-key/config/payments.ts/STRIPE_SECRET_KEY. When the value has no variable name (an
inline literal, a URL with credentials), use the enclosing symbol, or last4-<last 4> if there's none:
secrets-audit/github-pat/scripts/deploy.sh/last4-9f2c. No line numbers.skill: "secrets-audit". Run each candidate through the refutation pass in
references/verification.md, and rate with
references/severity-calibration.md (key types are in its section 6).
confirmed: verified live (reproduced: true when you made the provider check), or a secret-class key whose
authority you established. Severity follows the key type and blast radius. A hard-coded fallback secret in code
(process.env.JWT_SECRET || "changeme") is also confirmed. Rate it as if the default is live, add a
system_configuration condition, and cap confidence at medium ("Unsafe shipped defaults" in verification.md).needs_validation: you can't settle it without the owner, for example whether a found key is still active.
Record the blockers and the exact check they should run.rejected: revoked, a placeholder, publishable with correct restrictions, or a vendor's published example
value. ruled_out_by names what rules it out. location is where that's configured, or for an inert value the
line that holds it.~/.claude, ~/.codex, …) go in a separate
findings.user.json with paths relative to $HOME.# Secrets Audit Report
## Scope: [repos / environments / managers covered]
## Date: [date]
### Live leaked secrets found
| Provider | Key type (publishable / restricted / secret) | Redacted value | Location | First seen (commit / date) | Verified live? | Rotation status |
|---|---|---|---|---|---|---|
### Needs validation
| Provider | Location | Blocker | Check for the owner to run |
|---|---|---|---|
### Hardening notes (not findings)
- [e.g. publishable key without referrer restriction and no traced abuse; transcript retention unset]
### Rejected (do not re-raise)
| Fingerprint | Location | Ruled out by |
|---|---|---|
### Secrets-management posture
| Category | Status | Notes |
|---|---|---|
### Recommendations
| Priority | Item | Owner | Deadline |
|---|---|---|---|Dispositions for confirmed findings: Fixed / Deferred / Accepted Risk / Mitigated (platform/config) / Requires major migration (see owasp-audit). A rotated-and-revoked key is Fixed even if history still holds it.
gitleaks, trufflehog, detect-secrets documentationbriiirussell/cybersecurity-skills
c79bb8b
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.