Your OpenAI or Anthropic key is in your client bundle. Read this in the next 10 minutes.
sk- or sk-ant- appears in any file shipped to the browser, every visitor — and every scraper that fingerprints your page — can issue API calls on your account. They can drain your monthly budget in hours, read your prompt logs if usage history is enabled, and (if usage tier permits) hit production-tier rate limits to deny service to your real users. The five-step response: rotate the key first, scope the new one, audit usage, find every place the leaked key was committed, prevent it from happening again.
This is the AI-stack version of the same story we've covered for Supabase service_role keys and Stripe live keys. The shape is identical: a privileged credential meant to live on the server made it into src/, got compiled into the bundle, and is now public. The specifics differ — what an attacker can do, how billing works, what the rotation flow looks like — but the response order is the same.
Both OpenAI and Anthropic publish prefix-stable key formats specifically so that pattern scanners can find them. GitHub's secret scanning detects them. vibecheck detects them. Trufflehog detects them. So do the bots scraping every preview deploy on Vercel and every .pages.dev subdomain. The detection-to-exploitation window is measured in minutes, not days.
1. Rotate the key — right now, before reading the rest
OpenAI: platform.openai.com/api-keys → find the key (or all of them if you're not sure which) → click the trash icon → Revoke. The revoke is instant. Any in-flight request using that key gets a 401 within seconds.
Anthropic: console.anthropic.com/settings/keys → find the key → click Disable. Same instant-revoke semantics.
Generate a new key. Do not paste it anywhere yet — finish the audit first. If you've never set spend limits, set them now: OpenAI Console → Limits, Anthropic Console → Plans & Billing → Spend limits. A hard cap of $50 or $100 turns "drained my $5,000 budget overnight" into "got a notification at $50 and a stopped key."
Why now and not after the audit: every minute the leaked key is live is more potential abuse. Rotate first, then methodically clean up. The new key is useless to attackers because they don't know it exists.
2. Scope the new key
The leaked key was probably full-access. Don't replace it with another full-access key — both providers support scoped keys now, and scoping is the single biggest reduction in blast radius.
OpenAI restricted keys. When generating, choose Restricted instead of All. Per-resource permissions: Models (read/none), Model capabilities (write/read/none), Threads, Assistants, Files, Fine-tuning, Audio, Moderations, etc. For a typical chat app: Model capabilities write, everything else none. The key cannot be used for anything except calling models — no fine-tuning, no file uploads, no Assistants creation.
Anthropic Workspaces. Anthropic scopes keys via Workspaces. Console → Workspaces → create one per environment (production, staging, dev). Each workspace gets its own keys and its own spend limits. A leaked staging key cannot drain production budget.
For both: tag keys with where they live. Name the new key something like backend-prod-2026-05-09 or vercel-fn-claude-prod. When you find the next leak, the name tells you where the rotated key was deployed and what to update.
3. Audit usage during the exposure window
Both consoles surface real-time usage, but the granularity matters.
OpenAI: Console → Usage → filter by date range and by API key. Look for usage spikes that don't correlate with your traffic. Particularly: requests at 3 AM your timezone, requests against models you don't use (e.g., GPT-4 calls when your app uses gpt-4o-mini), surges in completion tokens with no surge in prompt tokens (sign of someone running scripted long-output generations).
Anthropic: Console → Usage → filter by date and key. Same anomaly patterns to look for. If you have the Admin API set up, you can pull per-key usage programmatically — useful when there are 30+ keys to audit.
If you find unauthorized usage:
- Document the window. Screenshot the usage graph. Note the first and last anomalous timestamp. This is what you'll need if you contact billing for a refund — both providers will sometimes credit a portion of fraudulent usage if you can demonstrate that the exposure was external (not a colleague spending too much).
- Check what was generated. If your app stores AI responses to a database, query for rows in the exposure window. Sometimes you can identify the abuse pattern (mass content generation, prompt-injection probing of your system prompt, keyword spam farms).
- Open a billing ticket. OpenAI: help.openai.com. Anthropic: support.anthropic.com. Reference the date range, the key ID (now revoked), and the screenshot. Be factual: "key was leaked in client bundle for X hours, here's the usage anomaly, here's the bundle commit that introduced it." Refunds are not guaranteed but are sometimes granted for unambiguous external compromise.
4. Find every place the leaked key was committed
Rotation revoked the live key. Now you have to make sure the literal string of the leaked value isn't somewhere it can be recovered or leak again under a different rotation.
Local repo:
git log --all -S "sk-" --oneline # OpenAI keys
git log --all -S "sk-ant-" --oneline # Anthropic keys
Every match is a commit that touched a string with that prefix. Inspect each. Note which branches the commit appears on.
GitHub-side: github.com/<you>/<repo>/search?q=sk- and sk-ant-. Catches everything in the default branch including issues and pull request descriptions, which git log -S won't. Also check forks if the repo was ever public — secret scanning notifies you, but external scrapers may have caught a window before notification.
If the leaked key was ever pushed to a public repo, force-push won't help. GitHub caches commits server-side; deletion from your branch doesn't delete the blob from forks or the cache. The only safe assumption is that the key is fully public the moment it pushed. Rotation is the actual fix; rewriting history is cosmetic.
CI logs and build artifacts: if you ever echo'd the key in a workflow (a common mistake when debugging "why isn't my env var loading"), the value is in your CI provider's logs. GitHub Actions: Settings → Actions → General → ensure secrets are masked. Past logs containing the key need to be deleted. Same for Vercel build logs, Cloudflare Pages, Netlify, etc.
Production deploys: if the key was in .env.local and got committed, every preview deploy that built from that commit has the key in its bundle. Rotate the key, then trigger fresh deploys for every preview to clean up the bundles.
5. Prevent the next leak
The leak happened because something with sk- in it ended up in a file that gets bundled to the browser. The pattern is consistent — the fix is making it harder to make the mistake.
Architectural rule: AI calls go through your server. No exceptions. Even for "small" features. The dev shortcut is to put the key in VITE_OPENAI_KEY or NEXT_PUBLIC_OPENAI_KEY "just for the prototype." Both prefixes mean ship to the browser. The prototype ships, the prefix is forgotten, the key is public.
Replace the shortcut with a tiny proxy:
// Next.js Route Handler — server-only, key never leaves the function.
// app/api/chat/route.ts
import OpenAI from "openai";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export async function POST(req: Request) {
// Validate session, rate-limit, scope what the user is allowed to ask.
const session = await requireSession(req);
await rateLimit(session.userId);
const { messages } = await req.json();
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: validateMessages(messages),
});
return Response.json(completion);
}
Same shape with fetch for any framework — the only thing that matters is that OPENAI_API_KEY is read on the server and never crosses the wire.
Three more layers, in order of effort:
- Rate-limit per user, not per IP. Otherwise an authenticated attacker can hit your endpoint at full pace. Cheap implementations: Upstash Ratelimit, Cloudflare Workers KV with a sliding-window counter, or a per-user Redis key.
- Set hard spend caps in both consoles. Even if everything else fails — leaked again, abused via your own endpoint, anything — the cap stops the bleed. $50 or $100 is enough for a personal project; tune per app revenue.
- Add a pre-commit hook that rejects committing files matching the key patterns. One line of
grepin.git/hooks/pre-commitor a tool like git-secrets. Catches the leak before it reaches GitHub. Won't catch the rare case where you commit and then realize, but covers the common case.
The deployed-side check
vibecheck scans deployed sites for both sk- (OpenAI) and sk-ant- (Anthropic) prefixes — plus Cohere, Mistral, Together, Replicate, Anyscale, fal, Deepgram, ElevenLabs, Perplexity, and Hugging Face legacy keys. Findings link to /fix/openai_key and /fix/anthropic_key for per-rule remediation. Run a scan against every preview deploy before merging — five seconds saves the rotation marathon.