Launch offer: everything is 100% off. Download any skill free, no sign-up. Get the skills
TechFixSkillsKey Leaked
Key Leaked

Key Leaked: find every copy of a leaked key and revoke it

Paste the "your key was exposed" alert and point it at your repo: the provider's revoke steps print first, then every place the key lives in your files, git history and build output. Before you push or deploy, it scans the same places and tells real leaks apart from keys that are public by design.

Free during launch. Works with Claude Code, Claude.ai, Cursor and Codex.

AgentsClaude Code, Claude.ai, Cursor, Codex
Read-onlyfixes only with your OK
Offlineno network calls, no account
NeedsPython 3.8+ (standard library)

When to use it

Use Key Leaked when a key got out, or before it can.

What you type, in plain words:

For CI and pre-commit hooks, run the scan script directly: scan.py <repo> --staged checks only what is about to be committed, and --since origin/main checks only new commits. Exit codes: 0 clean, 1 findings at a blocking tier, 2 tool error, 3 nothing blocking but the scan was incomplete.

How it works

  1. It picks a mode. An alert, an email or "my key leaked" means emergency mode. Nothing has happened yet means scan mode.
  2. Emergency: revoke steps first. Claude saves the alert text to a temporary file (and deletes it after, since it may hold the full key) and runs emergency.py --alert-file <file> --repo <repo>. The script prints the verdict and that provider's Revoke and Replace steps before it scans anything, read from one runbook file in references/runbooks/. Claude sends you those steps as soon as they are on screen, then follows up with every location, the rest of the runbook, the code fix and the other secrets it found.
  3. Scan: every place a secret can live. scan.py reads the current files, every commit on every branch, tag and stash, the local reflog, dangling objects, git notes, build output and source maps, and env files with browser prefixes. It sorts each finding into a tier: Exposed, At risk, Test mode, or Check these / Publishable. If it could not cover something (a shallow clone, Git LFS content, files over 64 MB), it says so and does not call the repo clean.
  4. Claude checks the work. It confirms findings by reading the lines, drops look-alikes, then does its own pass with the review checklist (references/review-checklist.md): Dockerfiles and compose files, CI workflows, infrastructure files, notebooks, mobile configs, deleted .env files in history, and values split or encoded in code. It promotes something only after confirming it is a real secret and where it is exposed.
  5. Verdict first, in plain words. For example: "2 secrets are exposed: an OpenAI key in your client bundle and a database password in git history". Findings are ranked by harm: keys that cost or move money, then data access, signing secrets, deploy tokens, then test-mode keys. Each one has a Finding, Evidence, Impact and Fix. A full secret is never printed: only the first characters and the last 4.
  6. Fixes, with your approval. Rotation comes first for anything exposed. The code change comes as a diff, applied only when you say so. History cleanup comes last and is optional. The scripts are read-only; the only file they can add is a one-line log entry in .techfix/ledger.md, and only with your OK.

Rotation is the real fix. A leaked key stops being dangerous when you revoke it at the provider and issue a new one. Deleting it from your code or rewriting git history does not un-leak it: forks, clones, CI logs and GitHub's cached views keep the old value. So Key Leaked always puts revoke-and-replace first. It shows the git filter-repo commands for cleaning history as an optional last step, and never runs them.

What it checks

27 known ways a secret ends up exposed, grouped here by where they hide.

In your repo and its history

In what ships to browsers

In config and infrastructure

Kinds of secret

What it does not flag as a leak

What it can't see

Worked examples

These are fixture projects from the skill's test suite, run with the real scripts. The values in them are fake test values, and every key-like value below is masked.

1. Before deploy: a Vite app leaking keys through its build

A Vite single-page app. The dist/ folder is committed and source maps are on. The chat feature calls Anthropic straight from the browser. A gitignored .env holds Firebase and PostHog keys, and the README has a sample curl command.

src/api/chat.ts
const r = await fetch("https://api.anthropic.com/v1/messages", {
  method: "POST",
  headers: {
    "x-api-key": "sk-ant-…_hAA",
    "anthropic-version": "2023-06-01",
    "anthropic-dangerous-direct-browser-access": "true",
  },
.env (gitignored) and vite.config.ts
VITE_FIREBASE_API_KEY=AIza…52Kv
VITE_POSTHOG_KEY=phc_…yi54

export default defineConfig({ build: { sourcemap: true } });
You
We deploy this tomorrow. Check it for secrets, including the built files.

Claude runs python3 scripts/scan.py vite-spa.

Script output (trimmed)
3 secrets exposed

Scanned 10 files (3 build output), 1 commits on 1 refs in 0.1s.

## Exposed (3)

### 1. Anthropic API key in dist/assets/index-3f9a1c2b.js and 1 more place
- **Finding:** Anthropic API key in dist/assets/index-3f9a1c2b.js and 1 more place: hardcoded in a source file; in build output that ships to browsers. First committed 2025-03-02 (6802c6f5), still at HEAD.
- **Evidence:** `sk-ant..._hAA`; dist/assets/index-3f9a1c2b.js:1 (build output, ships to browsers); src/api/chat.ts:5; commit 6802c6f5 2025-03-02: dist/assets/index-3f9a1c2b.js:1 (not pushed); commit 6802c6f5 2025-03-02: src/api/chat.ts:5 (not pushed)
- **Impact:** Readable by anyone who can read the repo or its history and anyone who loads your site or app, in its JavaScript or bundle. Anyone holding it can run paid Claude API calls billed to your account.
- **Fix:**
  1. Revoke it at Anthropic and issue a new one first; nothing else un-leaks it (`python3 /home/dev/.claude/skills/key-leaked/scripts/emergency.py --alert "Anthropic ending _hAA"` prints the steps).
  2. Rebuild and redeploy after removing it from the source; delete old build artifacts and CDN caches.
  3. Read it from an env var in server code (process.env.NAME / os.environ['NAME']).
…
### 2. Resend API key in dist/assets/index-3f9a1c2b.js
- **Finding:** Resend API key in dist/assets/index-3f9a1c2b.js: in build output that ships to browsers. First committed 2025-03-02 (6802c6f5), still at HEAD.
- **Evidence:** `re_...xser`; dist/assets/index-3f9a1c2b.js:1 (build output, ships to browsers); commit 6802c6f5 2025-03-02: dist/assets/index-3f9a1c2b.js:1 (not pushed)
…
### 3. SendGrid API key in dist/assets/index-3f9a1c2b.js.map
- **Finding:** SendGrid API key in dist/assets/index-3f9a1c2b.js.map: in build output that ships to browsers; inside a published source map. First committed 2025-03-02 (6802c6f5), still at HEAD.
- **Evidence:** `SG....QXhw`; dist/assets/index-3f9a1c2b.js.map (source map, original src/legacy/sendgrid.ts:3); commit 6802c6f5 2025-03-02: dist/assets/index-3f9a1c2b.js.map:1 (not pushed)
…
## Check these (1)

- Google Maps API key `AIza...v6RR` at src/components/StoreMap.tsx:4; commit 6802c6f5 2025-03-02: src/components/StoreMap.tsx:4 (not pushed): severity depends on restrictions this scan can't see.

**Publishable by design (info only), 2:** Firebase web config apiKey (.env); PostHog project API key (.env). Not leaks; restrict them in the provider dashboard where supported.

Ignored 1 placeholder, example or encrypted value (cause 22).
Fix (a diff, applied only with your approval)
@@ src/api/chat.ts @@
 export async function ask(prompt: string) {
-  const r = await fetch("https://api.anthropic.com/v1/messages", {
+  const r = await fetch("/api/chat", {
     method: "POST",
-    headers: {
-      "x-api-key": "sk-ant-…_hAA",
-      "anthropic-version": "2023-06-01",
-      "anthropic-dangerous-direct-browser-access": "true",
-    },
-    body: JSON.stringify({ model: "claude-sonnet-4-5", max_tokens: 512, messages: [{ role: "user", content: prompt }] }),
+    headers: { "content-type": "application/json" },
+    body: JSON.stringify({ prompt }),
   });
@@ vite.config.ts @@
-export default defineConfig({ build: { sourcemap: true } });
+export default defineConfig({ build: { sourcemap: false } });
@@ .gitignore @@
 node_modules
 .env
+dist

2. Before open-sourcing: a key that was "moved to env" but is still in history

An Express API, already pushed to a private GitHub repo. A while ago the Stripe live key was taken out of config/default.json in a commit called "move stripe key to env". Now the team wants to make the repo public. The current files look fine at a glance.

git log -p config/default.json (commit 00c3ab8)
 {
-  "port": 3000,
-  "stripe": {
-    "secretKey": "sk_live_…xjwU"
-  }
+  "port": 3000
 }
src/auth.js and .gitignore
const JWT_SECRET = "fcuttc…qoMG";

# .gitignore
node_modules
You
We're about to make this repo public. The Stripe key already moved to env, but check the whole history first.

Claude runs python3 scripts/scan.py express-api.

Script output (trimmed)
4 secrets exposed, 1 at risk, 1 test-mode key

Scanned 9 files, 3 commits on 2 refs in 0.1s.
…
### 3. Stripe live secret key in config/default.json
- **Finding:** Stripe live secret key in config/default.json: in a committed .env or config file; removed from the code but still in git history. First committed 2025-03-02 (b774740a), no longer in the current files.
- **Evidence:** `sk_liv...xjwU`; commit b774740a 2025-03-02: config/default.json:4
- **Impact:** Readable by anyone who can read the repo or its history. Access to your Stripe account within the key's scope: charges, refunds, customer data and payouts.
- **Fix:**
  1. Revoke it at Stripe and issue a new one first; nothing else un-leaks it (`python3 /home/dev/.claude/skills/key-leaked/scripts/emergency.py --alert "Stripe ending xjwU"` prints the steps).
  2. Rotation is the fix. Rewriting history (git filter-repo) is optional cleanup; forks and clones keep the old value.
- Causes: 2, 3. Fingerprint 8dc6b546fec4.
…
### 4. Signing secret (JWT_SECRET) in src/auth.js
- **Finding:** Signing secret (JWT_SECRET) in src/auth.js: hardcoded in a source file; a hardcoded signing secret. First committed 2025-03-02 (0b1bfdcf), still at HEAD.
- **Evidence:** `...qoMG`; src/auth.js:3; commit 0b1bfdcf 2025-03-02: src/auth.js:3
- **Impact:** Readable by anyone who can read the repo or its history. Anyone with it can forge sessions, tokens or webhook events and act as any user.
- **Fix:**
  1. Generate a new signing secret (eg `openssl rand -base64 32`), deploy it, then retire the old one; use your library's key-rotation or dual-key support if it has one. Old sessions or tokens stop working, which is intended.
…
## Test-mode secrets (rotate anyway) (1)

### 6. Stripe test-mode secret key in tests/payments.test.js
- **Finding:** Stripe test-mode secret key in tests/payments.test.js: in docs, a README, a notebook, a test fixture or a seed script. First committed 2025-03-02 (0b1bfdcf), still at HEAD.
- **Evidence:** `sk_tes...lYRB`; tests/payments.test.js:1; commit 0b1bfdcf 2025-03-02: tests/payments.test.js:1
- **Impact:** Test mode only: no real money moves, but it still opens test data and can be abused. Rotate it anyway. Access to your Stripe account within the key's scope: charges, refunds, customer data and payouts.
Fix (a diff, applied only with your approval)
@@ src/auth.js @@
-const JWT_SECRET = "fcuttc…qoMG";
+const JWT_SECRET = process.env.JWT_SECRET;
@@ src/server.js @@
-console.log("Booting with config", process.env);
+console.log("Booting, SENDGRID_API_KEY set:", Boolean(process.env.SENDGRID_API_KEY));
@@ tests/payments.test.js @@
-const stripe = require("stripe")("sk_test_…lYRB");
+const stripe = require("stripe")(process.env.STRIPE_TEST_SECRET_KEY);
@@ .gitignore @@
 node_modules
+.env

Then git rm --cached .env, so the file stays on your machine but leaves the repo from the next commit on.

3. Emergency: a GitHub secret scanning alert

A Next.js SaaS app, deployed on Vercel. A GitHub secret scanning alert arrives naming an OpenAI API key in src/lib/openai.ts. The developer pastes it into the chat.

src/lib/openai.ts
import OpenAI from "openai";

export const client = new OpenAI({
  apiKey: "sk-proj-…HvUE",
});
You
Got this from GitHub, what do I do?

GitHub secret scanning alert. Secret type: OpenAI API Key. Repository: next-saas. Location: src/lib/openai.ts, line 4. Detected in commit c2ea274 on main. Revoke this secret with the provider, then close the alert.

Claude saves the alert to a temporary file and runs python3 scripts/emergency.py --alert-file <tmp-file> --repo next-saas.

Script output (trimmed)
OpenAI key reported exposed: rotate it now. Revoke and replace steps below; locations follow after the repo scan

## Rotate now: OpenAI

_From references/runbooks/openai.json, checked 2026-10-13._

**Revoke**

1. Open the API keys page: https://platform.openai.com/settings/organization/api-keys (also linked as https://platform.openai.com/api-keys).
2. Find the leaked key by name and redacted value, then delete it.
3. For an Admin API key, open https://platform.openai.com/settings/organization/admin-keys and delete it there.
4. By API: DELETE /organization/projects/{project_id}/api_keys/{api_key_id} (project keys) or DELETE /organization/admin_api_keys/{key_id} (admin keys), authenticated with an Admin key.
5. Contact OpenAI Support via help.openai.com if you see activity you cannot explain.
6. If your account login may also be exposed, change your password, log out of all sessions and enable MFA.

**Replace**

1. Create a new secret key in the correct project from the API keys page.
2. Set an expiration date on the new project key.
3. Choose permissions in the key-creation dialog: Restricted or Read Only instead of the default All when possible.
4. Use a service-account key for servers and CI rather than a personal key.
5. Store the key in an environment variable or secrets manager on the server only.
…
_Scanning the repo for every location..._

## Result: 1 OpenAI key found in 2 places, first committed 2025-03-01; treat it as public and rotate now

## Where it lives (provider and file from the alert)

1. OpenAI project API key `sk-pro...HvUE` (exposed)
   - src/lib/openai.ts:4
   - commit c2ea274a 2025-03-01: src/lib/openai.ts:4
…
## History cleanup (optional)

Optional, and only after rotating. Rotation is the real fix: forks, clones, CI logs and GitHub's caches keep the old key no matter what you rewrite.
Commands are shown, never run. Needs git-filter-repo 2.47 or later. Merge or close open pull requests first.
```bash
git clone <your-repo-url> repo-clean && cd repo-clean
# write the OLD key into ../replacements.txt yourself, one per line, as: OLD_KEY==>REMOVED
git filter-repo --sensitive-data-removal --replace-text ../replacements.txt
git log --all -p | grep -c 'OLD_KEY_PREFIX'   # verify: expect 0
grep -c '^refs/pull/.*/head$' .git/filter-repo/changed-refs   # pull requests affected
git push --force --mirror origin
```
…
## What else is waiting

- exposed: OpenAI project API key at .env.local:2 (gitignored, but its browser prefix bundles it into client JS)
- exposed: Stripe live secret key at .env.local:1 (gitignored, but its browser prefix bundles it into client JS)
- exposed: Supabase service_role key at components/AdminPanel.tsx:4
- at risk: Code returns the whole environment in an HTTP response at app/api/debug/route.ts:4
Fix (a diff, applied only with your approval)
@@ src/lib/openai.ts @@
 export const client = new OpenAI({
-  apiKey: "sk-proj-…HvUE",
+  apiKey: process.env.OPENAI_API_KEY,
 });

The new key goes into Vercel's environment variables (and your local .env.local, without a NEXT_PUBLIC_ prefix), then you redeploy.

Results

Blind test (Oct 13): 35 planted leaks across 10 projects. Key Leaked caught all 35 with 0 false alarms. gitleaks (a popular free scanner) with default rules on the same test: 60% caught, 25 false alarms.

Blind test, Oct 13 (35 planted leaks, 10 projects)Key Leakedgitleaks, default rules
Planted leaks caught35 of 3560%
False alarms025

Revoke-and-replace steps for 29 providers, each with its source and the date it was checked.

Install Key Leaked

Download key-leaked.zip Free during launch. Checksums

You need Python 3.8 or newer where your agent runs its commands, because the checks are small Python scripts (standard library only, nothing to pip install). macOS and Linux usually have python3 already; on Windows install it from python.org. Git is needed for the history checks.
Claude Code
  1. Download key-leaked.zip (button above).
  2. Unzip it into your personal skills folder so it works in every project:
    unzip ~/Downloads/key-leaked.zip -d ~/.claude/skills/
    Or into one project only (commit it to share it with your team):
    unzip ~/Downloads/key-leaked.zip -d .claude/skills/
  3. Check it landed:
    ls ~/.claude/skills/key-leaked/SKILL.md
  4. Use it. Claude Code picks up new skills without a restart (if ~/.claude/skills did not exist when your session started, run /reload-skills). Ask in plain words, for example “I pushed my .env to GitHub, help” or paste the "your key was exposed" email, or type /key-leaked.
Claude.ai (web and desktop app)
  1. Open Settings > Capabilities and make sure Code execution and file creation is on. Skills need it. (Available on Pro, Max, Team and Enterprise plans; on Enterprise an Owner enables Skills first.)
  2. In the Skills section, click Upload skill and choose key-leaked.zip. Do not unzip it.
  3. Check that it appears in your Skills list and switch it on.
  4. Start a chat, attach your project as a zip (or paste the error or alert) and ask, for example “I pushed my .env to GitHub, help”. In Claude.ai the scripts run in Claude’s sandbox on the files you upload, not on your computer.
Cursor
  1. Unzip into Cursor’s skills folder for all projects:
    unzip ~/Downloads/key-leaked.zip -d ~/.cursor/skills/
    or into a project: .cursor/skills/. Cursor also reads ~/.claude/skills/, so a Claude Code install works in Cursor too.
  2. In Agent chat, type / and pick key-leaked, or just ask: “I pushed my .env to GitHub, help”.
OpenAI Codex
  1. Unzip into your user skills folder:
    unzip ~/Downloads/key-leaked.zip -d ~/.agents/skills/
    or into .agents/skills/ at the root of a repo.
  2. Mention it with $key-leaked (or pick it from /skills), or just ask: “I pushed my .env to GitHub, help”.
Other agents, updates and removal
  • Any agent that reads SKILL.md files: unzip into its skills folder. Each skill is a folder with SKILL.md, scripts/ and references/.
  • Update: download the new zip and unzip it over the old one with unzip -o.
  • Remove: delete key-leaked/ from the skills folder.
  • Check the download: shasum -a 256 key-leaked.zip and compare with SHA256SUMS.txt.

FAQ

Does it change my code?

Not on its own. The scripts are read-only. Claude shows each fix as a diff and applies it only when you approve. It never rewrites your git history: it shows the commands and explains why rotating the key comes first. The one file it can add is a log line in .techfix/ledger.md (date, mode, counts, salted fingerprints, never a secret), only with your OK, and that folder gets its own .gitignore so it is never committed.

Does my code or my key leave my machine?

The scripts run locally, use only Python's standard library and git, and make no network calls. They never print a full secret, so the output Claude reads holds only masked values like sk-pro...HvUE. Claude still reads files in your repo as it would for any task. If you paste a full key, Claude refers to it by its last 4 characters and never repeats it.

Which stacks does it work with?

It reads files and git, not frameworks, so it works on any repo. It knows the browser-prefix rules of Next.js, Vite, SvelteKit, Create React App, Expo, Nuxt and Gatsby, and the config files of Python, .NET, Java and Node projects, Docker, Kubernetes, CI services and hosts like Vercel, Netlify, Cloudflare, Fly and Render. You need Python 3.8 or newer and git.

How long does it take?

The scripts take 0.2 to 0.35 seconds on these examples; Claude's review adds time. In an emergency, the revoke steps print before the repo scan starts. For big repos, --summary gives a short report.

How is this different from asking my agent to look for keys?

An agent asked directly usually searches the current files. Key Leaked also reads every commit on every branch, tag and stash, the local reflog, build output and source maps, and tells real secrets apart from keys that are public by design. Its revoke steps come from dated runbooks for 29 providers instead of memory, it puts rotation before cleanup, and it never prints a full secret.

What does it cost?

Key Leaked is normally $7. It is free during launch.