If you have 30 seconds: Rotate the key right now. Go to your provider dashboard, delete the exposed credential, generate a new one. The rest of this guide tells you what to do after that — but rotation is the one action that actually neutralizes the threat. Deleting from git is not enough.
GitHub is continuously crawled by automated bots that search for credential patterns. Research from GitGuardian found that secrets committed to public repositories are detected within an average of four seconds. For private repositories, the window is longer — but private repos get breached too, and former collaborators retain git history access after being removed.
A Stripe live key means an attacker can issue refunds, create charges, access customer payment data, and spin up webhook endpoints. An OpenAI key costs money per token — and attackers will run it at full speed until the credit runs out. An AWS access key can provision infrastructure, access S3 buckets, create IAM users, and pivot deeply into your cloud environment.
Treat this like a breach. Move fast, document what you find, and make sure it can't happen again.
In priority order. Do not skip ahead.
Deleting the key from your repo is not enough. The key lives in git history forever, and GitHub's index may have cached it. The only action that neutralizes the exposure is invalidation. Go to the provider dashboard right now and revoke the exposed key, then generate a fresh one. Do not use the old value anywhere.
The commit that added the file is still in history. Anyone who cloned before you removed it has a local copy. If your repo is public, assume it has been scraped within minutes. Run a quick history grep to understand the full scope:
# Search all commits for common credential patterns git log --all --full-history -p \ | grep -iE "(sk-|api_key|secret|token|password|aws_|stripe_)" \ | head -60 # See which files have ever contained secrets: git log --all --full-history --diff-filter=D -- "*.env" "*.key" "*.pem" # Find the exact commits: git log --all --oneline -S "sk-proj-"
Even after rotation, document which commits contained secrets. This audit log is useful if you need to assess scope of exposure or notify users.
A single exposed API key is almost never the only one. AI editors hardcode credentials, copy-paste from docs leaves placeholders, and environment variables get inlined during debugging sessions. Every project we scan with active security incidents has at least two or three separate secret findings. Run a full scan to find everything at once:
# Install and scan — no config needed npx custodia-cli scan # Output: every secret pattern across 40+ providers # File paths, line numbers, severity, and rotation links
Custodia scans for 40+ credential patterns: OpenAI, Anthropic, Stripe, AWS, Twilio, Sendgrid, GitHub tokens, JWT secrets, database connection strings, and more.
After rotating and finding every exposed key, move them all to environment variables. Never let a credential touch source code again.
// ❌ The pattern that caused this incident
// File: src/lib/stripe.ts
import Stripe from 'stripe';
const stripe = new Stripe('sk_live_4xKy9mR3...'); // DO NOT DO THIS
const openai = new OpenAI({ apiKey: 'sk-proj-K9...' }); // DO NOT DO THIS
// Committed → pushed → public → scraped// ✅ Environment variable pattern — safe to commit
// File: src/lib/stripe.ts
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// ─────────────────────────────────────────
// .env.local (add to .gitignore — never commit)
// STRIPE_SECRET_KEY=sk_live_...
// OPENAI_API_KEY=sk-proj-...
//
// .env.example (commit this — no real values)
// STRIPE_SECRET_KEY=your_stripe_secret_key_here
// OPENAI_API_KEY=your_openai_api_key_hereThe root cause is usually that there was no automated check between "write code" and "ship it." Add two gates:
# Option 1: git-secrets (simple, widely used) brew install git-secrets git secrets --install git secrets --register-aws # Option 2: detect-secrets (regex-based, more patterns) pip install detect-secrets detect-secrets scan > .secrets.baseline # Add pre-commit hook: detect-secrets-hook --baseline .secrets.baseline
# .github/workflows/security.yml
# Scans only changed files — fast, no noise
- name: Custodia diff scan
run: npx custodia-cli scan --diff
env:
CUSTODIA_API_KEY: ${{ secrets.CUSTODIA_API_KEY }}One leaked key usually means more. Custodia scans your codebase against 40+ credential patterns — OpenAI, Stripe, AWS, Twilio, Anthropic, GitHub tokens, JWT secrets, and more.
# Scan your full codebase for exposed secrets npx custodia-cli scan # Output: every secret with file path, line number, provider match
┌──────────────────────────────────────────────────────┐ │ CUSTODIA.DEV // SECRET SCAN RESULTS │ └──────────────────────────────────────────────────────┘ [SCAN] Scanning 284 files for credential patterns... ── SECRETS FOUND ──────────────────────────────────── [SECRET] CRITICAL OpenAI API Key src/lib/openai.ts:4 → sk-proj-K9m2... Action: Rotate at platform.openai.com [SECRET] CRITICAL Stripe Live Secret Key src/lib/stripe.ts:3 → sk_live_4x... Action: Roll key at dashboard.stripe.com [SECRET] HIGH JWT Signing Secret (weak entropy) src/middleware.ts:12 → "mysecretkey123" ───────────────────────────────────────────────────── SECRETS FOUND: 3 · 2 critical · 1 high REPORT: custodia.dev/reports/secrets_7mK3p
API key exposure happens through a specific workflow: developer uses an AI editor to scaffold a feature, the AI generates working code with a hardcoded credential similar to what it's seen in training data, developer tests it and it works, git add, git commit. The key is in history before anyone noticed.
The pattern repeats for every external service. If you found a Stripe key hardcoded, there's a good chance there's also an OpenAI key, a Twilio key, a database URL, and a JWT signing secret living somewhere in the codebase — maybe in an old utility file, maybe in a config that never got added to .gitignore.
Scan now and find everything. It's faster than a manual audit and more thorough than a grep for a single pattern.
Act immediately in this order: (1) Rotate the key — go to the API provider dashboard right now and invalidate the exposed key, then generate a new one. Deletion from git is NOT enough. (2) Audit git history — run `git log -p | grep -i "sk-\|api_key\|secret"` to find all commits that touched secrets. The key lives in history even after you delete the file. (3) Scan for more secrets — use a tool like Custodia to scan your entire codebase, because one leaked key usually means there are more. (4) Add .gitignore entries and move all secrets to environment variables. (5) Enable pre-commit hooks or CI scanning to prevent future commits.
No. Deleting the file or pushing a commit that removes the key does not remove it from git history. Anyone who cloned your repo before deletion still has the key in their local history. GitHub's cache and search indexes may also retain the content temporarily. The only safe path is to rotate (invalidate) the exposed key immediately, then treat any previous value as permanently compromised — regardless of whether you removed it from the repository.
For a manual audit: run `git log --all --full-history -p | grep -iE "(sk-|api_key|secret|password|token|aws_|stripe_|openai)" | head -100`. This searches every commit in your history for common credential patterns. For a thorough automated scan: `npx custodia-cli scan` checks your current codebase against 40+ secret patterns across all major providers (OpenAI, Stripe, AWS, Twilio, Anthropic, Sendgrid, and more). For history scanning specifically, tools like `git-secrets` or `truffleHog` can scan the entire git history.
Three layers of prevention: (1) Never hardcode keys — always use environment variables and load them from .env files that are in .gitignore. Commit a .env.example with placeholder values instead. (2) Add a pre-commit hook — use `git-secrets` or `detect-secrets` to block any commit containing credential-shaped strings before it reaches git history. (3) Add CI scanning — run `npx custodia-cli scan --diff` on every PR to catch secrets in changed files before they merge to your main branch.
Assume the worst: the key has been scraped. Bots continuously crawl GitHub for credentials — keys in public repos are typically found within minutes to hours. Rotate the key immediately regardless of age. Then: check the API provider's usage logs for any requests you don't recognize. If the provider allows it, add IP restrictions or spending alerts to the new key. Review your account for any resources created or data accessed without your authorization. Consider the incident a breach and document what was exposed for your records.
40+ secret patterns. OWASP Top 10. Free — 3 scan credits. See pricing →