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.
When to use it
Use Key Leaked when a key got out, or before it can.
- You got a GitHub secret scanning alert, a GitGuardian email, or an "exposed credentials" email from OpenAI, Anthropic, AWS, Google, Stripe, Slack or a similar provider.
- You pushed your
.env, or a key sat in a commit that you later "fixed" by deleting the line. - You want to know how to get a secret out of git history.
- You are about to push, open-source or deploy, and want to check the repo first.
- You put a key in a
NEXT_PUBLIC_orVITE_variable, or call OpenAI (or another paid API) from browser code, and want to know if that is a problem. - You want a CI job or pre-commit hook that blocks secrets.
What you type, in plain words:
- "I pushed my .env to GitHub, help", or paste the alert email as it came.
- "Check this repo for secrets before I deploy."
- Or type
/key-leaked.
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
- It picks a mode. An alert, an email or "my key leaked" means emergency mode. Nothing has happened yet means scan mode.
- 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 inreferences/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. - Scan: every place a secret can live.
scan.pyreads 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. - 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.envfiles 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. - 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.
- 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
- Keys hardcoded in source, under any variable name, including values split with
+or passed through base64. - Committed
.env,.env.productionand config files (config/*.json,settings.py,appsettings.json,application.yml). - Keys removed from the code but still in an old commit.
- Other branches, tags and stashes, plus the local reflog, dangling objects (a file staged once, a dropped stash), git notes and checked-out submodules.
- Commits that no remote has yet, labelled "committed but not pushed yet", so you can take the value out before it leaves your machine.
In what ships to browsers
- Build output:
dist/,build/,out/,.next/static/,.output/public/. - Source maps that carry your original code, with the secret inside.
- Secret keys behind
NEXT_PUBLIC_,VITE_,PUBLIC_,REACT_APP_,EXPO_PUBLIC_,NUXT_PUBLIC_orGATSBY_, even in a gitignored env file. - Paid APIs called from browser code (
dangerouslyAllowBrowser, direct calls to OpenAI or Anthropic), and a Supabaseservice_rolekey in client code.
In config and infrastructure
- CI workflows with literal values instead of
${{ secrets.X }}. - Dockerfile
ENVandARG, compose files, KubernetesSecretobjects (base64 is not encryption). - Host configs:
vercel.json,netlify.toml,wrangler.toml,fly.toml,render.yaml,serverless.yml,app.yaml. - Mobile configs (
app.jsonextra,eas.json),.npmrctokens,.pypirc, Docker and AWS credential files.
Kinds of secret
- Provider API keys by format, even without a telling variable name.
- Private keys and service-account JSON files.
- Database URLs with a password in them.
- Signing secrets: JWT, session and webhook secrets.
- Code that logs or returns your whole environment (
console.log(process.env), debug routes).
What it does not flag as a leak
- Keys that are public by design: Stripe
pk_, Supabase anon keys, Firebase web config, Sentry DSNs, PostHogphc_, Clerkpk_, Algolia search-only keys, Mapboxpk.tokens. They are listed as Publishable, info only. - Placeholders (
your-key-here,changeme,sk-...) and provider-documented example keys. - Encrypted files (SOPS, SealedSecret, Ansible Vault, Rails
credentials.yml.enc), hashes, UUIDs, public keys and certificates. - A value that only has a secret-like name and sits only in a test, an example file or a local compose file goes to "Check these", with a one-line reason. A real provider-format key stays Exposed even there.
What it can't see
- Whether a key is still live, or was used. It never makes network calls; it assumes an exposed key is live. Check the provider's usage page (the runbook links it).
- Copies outside the repo: forks, clones, GitHub's cached views, CI logs, chat pastes. This is why rotation is the fix.
- Restrictions set at the provider (referrer or IP limits, scopes, spend caps).
- Secrets that live only in host dashboards or CI secret stores, and deleted remote branches that were never fetched.
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.
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 } });
Claude runs python3 scripts/scan.py vite-spa.
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).
What the skill tells Claude to do next: post the verdict first ("3 secrets are exposed, and anyone who loads your site can read them in its JavaScript"), then confirm each one by reading the line. The Anthropic key is hardcoded in src/api/chat.ts and copied into the bundle. The Resend key is only in the committed bundle. The SendGrid key is inside the source map, which embeds an original file (src/legacy/sendgrid.ts). All three are rotated first.
Look-alikes are not treated as leaks. The Firebase web apiKey and the PostHog key are public by design, so no rotation. The Google Maps key goes under "Check these": Claude tells you to check its referrer restriction in Google Cloud instead of calling it a leak. The README's sk-ant- placeholder is ignored. Claude then walks the checklist (build output, source maps, browser calls to paid APIs) and offers the diff below, plus a server route that reads the key from an env var. After you approve: rebuild, stop committing dist/ (git rm -r --cached dist), redeploy.
@@ 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.
{
- "port": 3000,
- "stripe": {
- "secretKey": "sk_live_…xjwU"
- }
+ "port": 3000
}
src/auth.js and .gitignore
const JWT_SECRET = "fcuttc…qoMG";
# .gitignore
node_modules
Claude runs python3 scripts/scan.py express-api.
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.
What the skill tells Claude to do next: give the verdict first ("4 secrets are exposed, including a Stripe live key that was removed from config/default.json but is still in commit b774740a"), and say plainly that moving the key to env did not remove it. Making the repo public with this history would hand it to anyone, and the checklist says to assume bots find a key in a public repo within minutes.
Claude ranks the findings the way the checklist says: the Stripe live key and the SendGrid key first (money and email in your name), then the MongoDB password (data), then the JWT secret (rotating it signs everyone out, and it says so). The .env was committed because .gitignore lists only node_modules. The Stripe test key gets rotated too, with the reason. The console.log(process.env) line is At risk: not public yet, but the next log line would leak every secret. Had these commits never been pushed, the report would say "committed but not pushed yet", and the cheap fix would be to amend or rebase before pushing.
After the keys are rotated, Claude can show the history cleanup commands from the runbook (git filter-repo, then a force push and a note to GitHub Support). They are optional, and the skill never runs them.
@@ 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.
import OpenAI from "openai";
export const client = new OpenAI({
apiKey: "sk-proj-…HvUE",
});
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.
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
What the skill tells Claude to do next: as soon as the Revoke and Replace part is on screen, send a first message with the verdict in one line ("your OpenAI key is exposed: rotate it now") and the revoke steps in order. The scan has not finished yet at that point, and it does not need to.
Then a second message, in this order: both locations (the current file and commit c2ea274a); the rest of the runbook (update the key in every host and CI store, redeploy, check usage for the exposure window); the code fix as a diff, for approval; history cleanup last and optional, as commands it never runs; and the other secrets, one line each. Here that is a second OpenAI key and a Stripe live secret key behind NEXT_PUBLIC_ in .env.local (gitignored, but bundled into client JavaScript), a Supabase service_role key in a client component, and a debug route that returns the whole environment. The Stripe pk_live_ key on the checkout page and the Supabase anon key are not on that list: a plain scan of the same repo lists them as publishable by design. Claude deletes the temporary alert file when done.
@@ 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 Leaked | gitleaks, default rules |
|---|---|---|
| Planted leaks caught | 35 of 35 | 60% |
| False alarms | 0 | 25 |
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
python3 already; on Windows install it from python.org. Git is needed for the history checks.Claude Code
- Download
key-leaked.zip(button above). - Unzip it into your personal skills folder so it works in every project:
Or into one project only (commit it to share it with your team):unzip ~/Downloads/key-leaked.zip -d ~/.claude/skills/unzip ~/Downloads/key-leaked.zip -d .claude/skills/ - Check it landed:
ls ~/.claude/skills/key-leaked/SKILL.md - Use it. Claude Code picks up new skills without a restart (if
~/.claude/skillsdid 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)
- 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.)
- In the Skills section, click Upload skill and choose
key-leaked.zip. Do not unzip it. - Check that it appears in your Skills list and switch it on.
- 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
- Unzip into Cursor’s skills folder for all projects:
or into a project:unzip ~/Downloads/key-leaked.zip -d ~/.cursor/skills/.cursor/skills/. Cursor also reads~/.claude/skills/, so a Claude Code install works in Cursor too. - In Agent chat, type
/and pick key-leaked, or just ask: “I pushed my .env to GitHub, help”.
OpenAI Codex
- Unzip into your user skills folder:
or intounzip ~/Downloads/key-leaked.zip -d ~/.agents/skills/.agents/skills/at the root of a repo. - 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.mdfiles: unzip into its skills folder. Each skill is a folder withSKILL.md,scripts/andreferences/. - 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.zipand 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.