In short
- A leaked key works until the provider rejects it. Deleting it from your code, git history or bundle does nothing to the copies already taken.
- The order: put a new key where it cannot leak (a server secret), deploy, revoke the old key, prove the provider rejects it, then clean the code and history. If the bill is climbing, revoke first and accept the downtime.
- Review the window between the leak and the revocation: OpenAI and Anthropic usage pages, Stripe request logs in Workbench, AWS CloudTrail, your Supabase tables and users.
- Some keys are revoked for you: GitHub revokes its own tokens pushed to public repositories, and Anthropic deactivates Claude keys GitHub reports. A key in your frontend bundle gets no such help.
- To stop the next one, call paid APIs from a server, keep push protection and a secret scanner on, and set hard spend limits.
On this page
- What to do first when an API key leaks
- Why deleting the key from your code does not un-leak it
- The order: rotate, revoke, then fix the code
- Check for abuse: usage, billing and logs
- Per-provider steps for the keys that leak most
- When the key was in your frontend bundle
- How to stop the next leak
- Questions
- Sources
What to do first when an API key leaks
A leaked API key keeps working for anyone who copied it until the provider refuses it, whatever you change in your own code. So the job, in order: put a new key somewhere it cannot leak, switch your app to it, revoke the old key, prove the old key fails, look for what was done with it, and only then clean up the code and the git history. GitHub's guidance on leaked credentials starts the same way: revoke or rotate the secret first, because a revoked credential can no longer be used, and that may be all you need.
If you only have a minute, open the provider's dashboard, find the key and revoke it. Your app may break until a new key is in place. If the usage page shows calls you did not make, that trade is worth it. Supabase's rotation guide agrees: if someone is actively using the leaked key, retire it right away, even if parts of your app stop working until you replace it.
Why deleting the key from your code does not un-leak it
Removing the line and pushing a new commit feels like the fix. It only changes the newest version of one file. The key itself, the string, is now in places you do not control:
| Where the old key still lives | Why it survives | What actually helps |
|---|---|---|
| Git history | Every earlier commit still holds the file as it was, and anyone who clones the repository gets all of them. | Revoking the key. Rewriting history comes later, if at all. |
| Forks, clones and pull requests | GitHub lists clones, forks, cached views of old commits and pull requests that reference them among the places sensitive data persists. You cannot clean other people's clones. | Revoking the key. GitHub Support can remove cached views and pull request references. |
| Old deployments | On Vercel, every deployment gets its own URL that stays reachable for as long as your retention policy keeps it, and is public unless deployment protection covers it. Each one serves the bundle it was built with. | Revoking the key, then deleting old deployments or turning on deployment protection. |
| Visitors' browsers and caches | A key in your JavaScript was downloaded by every visitor, and cached copies of the old file do not change when you redeploy. | Revoking the key. |
| Chats, tickets, logs, screenshots | A key pasted into a support thread, an AI chat or a log line has leaked too. | Revoking the key. |
The order: rotate, revoke, then fix the code
Rotating means creating a replacement and switching your app to it. Revoking means telling the provider to reject the old key. OpenAI's documentation describes this sequence for rotating a key: issue a replacement, update the workload, verify that it works, and revoke the previous key. After a leak you run the same steps with the waiting cut to minutes.
- 1
Decide: revoke now, or rotate first
Open the provider's usage page. If you see calls, charges or objects you did not make, revoke the key immediately and accept the downtime. If not, rotating first keeps your app running, as long as the next steps take minutes, not days.
- 2
Create the new key where it cannot leak
Store it as a server secret: your host's environment settings for server code, an Edge Function secret or a secret manager. Never in a variable starting with
VITE_orNEXT_PUBLIC_: the build copies those into the JavaScript every visitor downloads. If the old key leaked through your frontend, the new one must not take the same path. Supabase puts it bluntly: fix the root cause before you rotate, or the new key leaks the same way. - 3
Deploy and verify
Redeploy, then confirm the app works with the new key. Stripe, for example, shows request logs for each key, so you can watch the new key take traffic before you kill the old one.
- 4
Revoke the old key
Delete, deactivate or expire it at the provider. Some dashboards offer a grace period in which both keys work: when you rotate a Stripe key, the old one can keep working for up to 7 days. After a leak, set the expiration to Now.
- 5
Prove the old key is dead
Send one request with the old key and check that it fails. Keep the response and the time: if anyone asks when the key stopped working, you have the answer.
- 6
Then fix the code and the history
Remove the key from the code, move the call to a server if it ran in the browser, delete old deployments that still serve it, and decide whether the git history needs rewriting. GitHub's view is that once the credential is revoked or rotated, rewriting history may not be warranted.
To test the old key from your terminal, read it without echoing it, so it stays out of your shell history, then call an endpoint that only reads:
read -rs OLD_KEY # paste your own old key, then press Enter
# OpenAI: lists models, runs nothing
curl -s -o /dev/null -w "%{http_code}\n" https://api.openai.com/v1/models \
-H "Authorization: Bearer $OLD_KEY"
# Anthropic: lists models, runs nothing
curl -s -o /dev/null -w "%{http_code}\n" https://api.anthropic.com/v1/models \
-H "x-api-key: $OLD_KEY" -H "anthropic-version: 2023-06-01"
# Stripe: reads the balance, changes nothing
curl -s -o /dev/null -w "%{http_code}\n" https://api.stripe.com/v1/balance \
-u "$OLD_KEY:"
unset OLD_KEY200 means the key still works. 401 means the provider rejects it. A Stripe restricted key (rk_) without balance access returns 403: that key is still valid.Check for abuse: usage, billing and logs
Write down two times: when the key was first exposed (the commit, the deploy, the moment you pasted it somewhere) and when you revoked it. Everything in between is the window to review. Start with what the key could reach: AWS's advice for an exposed access key is to work out which resources those credentials can access, since an IAM user's key carries that user's permissions, while a root user key has full access to every resource in the account.
If the key was committed, git can tell you when it first appeared. Search for a fragment of the key rather than the whole string:
# Use 8 or more characters from the middle of the key:
# prefixes such as sk- are shared by every key of that provider
git log --all --reverse -S "PART_OF_THE_KEY" --format="%h %ad %an %s" --date=iso | head -n 1| Provider | Where to look | What to look for |
|---|---|---|
| OpenAI | The Usage page of the platform, for the project the key belonged to | Spend or token volume above your normal days, models your app does not use |
| Anthropic | The Logs and Usage pages of the Claude Console | Requests and spend you cannot match to your app |
| Stripe | View request logs on the key, which opens Workbench, then recent payments, refunds and customers | Requests you cannot match to your servers, refunds or objects you did not create |
| AWS | CloudTrail, which records management events for 90 days by default, and the bill | New IAM users, roles or access keys, and resources in regions you never use |
| Supabase (secret or service role key) | Your tables, storage buckets and the users list | Rows, files or accounts created, changed or deleted in the window |
| GitHub token | The repositories, workflows and settings the token could reach | Commits, releases, workflow edits or new keys you did not add |
If you find abuse, write to the provider's support with the key's name or ID (never the key itself), the time window and what you saw. Then deal with what the key could reach. A Supabase service role key skips every row level security policy, so a leak there is a data incident: the service role key guide covers what to check in the database.
Per-provider steps for the keys that leak most
| Provider | Kill the old key | Worth knowing |
|---|---|---|
| OpenAI | On the API keys page, revoke the key and create a new one, ideally owned by a project service account rather than your own user (OpenAI docs). | Set a hard spend limit on the project. Service account keys can carry an expiration date. |
| Anthropic | In the Claude Console, open API keys, then the menu next to the key, then Delete API key (Anthropic help). | GitHub reports Claude keys found in public repositories, and Anthropic deactivates them automatically. Since October 2026, push protection blocks the new Anthropic key types by default in repositories with secret scanning on, free public ones included. |
| Stripe | On the API keys page of the Dashboard, open the menu on the key, choose Rotate key, set Expiration to Now (the old key is deleted), then Rotate API key (Stripe docs). The new key works at once. | The publishable key (pk_) is meant for the browser. Secret (sk_) and restricted (rk_) keys are not. Stripe recommends restricted keys, and access policies that only accept requests from your servers. |
| AWS | In IAM, deactivate the access key, then delete it once nothing breaks. AWS suggests disabling before deleting because a disabled key can be turned back on (AWS Security Blog). | AWS may attach the AWSCompromisedKeyQuarantineV3 policy to the user and open a support case. Treat that as an alarm, not a cleanup: the key exists until you deactivate and delete it. Temporary credentials already issued with the key can keep working after you disable it, for up to 36 hours; AWS's post explains how to invalidate them. |
| Supabase | Secret key (sb_secret_): create a new one under Settings > API Keys, switch your servers, delete the old one. Legacy service_role key: move browsers to a publishable key, deactivate the legacy keys, then revoke the legacy JWT secret (Supabase docs). | The anon and publishable keys are public by design. Rotating them protects nothing; row level security does. See the anon key guide. |
| GitHub | A personal access token, OAuth token or GitHub App token pushed to a public repository or public gist is revoked automatically (GitHub docs). Anywhere else, revoke it yourself in your GitHub settings. | Give personal access tokens an expiration date: GitHub revokes a token automatically when it expires, so a forgotten copy stops working on its own. |
When the key was in your frontend bundle
One way a key leaks from an app built with an AI coding agent is a provider call made from the browser, with the key in a VITE_ or NEXT_PUBLIC_ variable. It works on the first try and ships the key to every visitor. OpenAI's documentation is explicit: keep secret keys out of browser code, mobile apps, source control, logs and customer-facing responses.
// Before: runs in the browser, so the key ships to every visitor
const res = await fetch("https://api.openai.com/v1/responses", {
method: "POST",
headers: {
Authorization: `Bearer ${import.meta.env.VITE_OPENAI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ model: "your-model", input: prompt }),
});// After: app/api/ai/route.ts runs on the server, the key stays there
import { getUser } from "@/lib/auth"; // your own session check
export async function POST(req: Request) {
const user = await getUser(req);
if (!user) return new Response("Sign in first", { status: 401 });
const { prompt } = await req.json();
const res = await fetch("https://api.openai.com/v1/responses", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ model: process.env.OPENAI_MODEL, input: String(prompt).slice(0, 4000) }),
});
return new Response(res.body, { status: res.status });
}
// In the browser, call your own route instead:
// await fetch("/api/ai", { method: "POST", body: JSON.stringify({ prompt }) });OPENAI_API_KEY without any public prefix. In a Vite app, put the same logic in a serverless or Edge Function.Moving the call is half the fix. A server route that anyone can call without signing in, with no rate limit, is the same leak one step removed: strangers spend your credits through it instead of with your key. The unprotected AI endpoint guide covers what that route needs.
Not every key in a bundle is a leak. Stripe's publishable key (pk_) and Supabase's publishable or anon key are made for browsers; the risk there sits in your server code and your row level security, not in the key. Our guide to API keys exposed in the frontend sorts the public keys from the secret ones. And if the key leaked because your host served your .env file, every key in that file needs rotating, not only the one you noticed: see exposed .env files.
How to stop the next leak
- Call paid APIs from a server function that checks the user is signed in. The browser gets only keys built to be public.
- Keep secrets in your host's environment settings or a secret manager, and add
.envto.gitignorebefore the first commit. - Leave GitHub push protection on. Push protection for users is on by default and stops you pushing supported secrets to public repositories; turn it on for the repository as well, so a bypass creates an alert.
- Run a secret scanner such as Gitleaks in CI, as Anthropic recommends, so a key in a pull request is caught before it merges.
- Use one key per app and per environment. Anthropic suggests separate keys for development, testing and production, so a leak means disabling one use, not all of them.
- Cap the spend: a hard spend limit on each OpenAI project, a deliberate auto-reload threshold on Anthropic credits, a budget alert on every cloud account.
- Prefer restricted and short-lived credentials: Stripe restricted keys with an access policy, AWS IAM roles that issue temporary credentials instead of long-lived access keys, OpenAI service account keys with an expiration.
- Before each launch, look at what your live app serves, not only at your repository.
That last point is where repository scanners stop. They find the keys you committed; they do not show what your deployed app hands to every visitor, which is where a key in a VITE_ variable ends up. When Keybrum attacks an app, it reads the JavaScript the live site serves, looks for keys such as OpenAI, Stripe, a Supabase service role key or a private key, and, where the provider allows it, tests the key with a read-only call. For an OpenAI or Stripe key, the finding says whether the key still works, not only that it was found.
Questions
What should I do if my OpenAI API key leaked?
Revoke it on the API keys page of the OpenAI platform, create a new key and store it only on your server. Check the Usage page for spend you cannot explain since the key was exposed. Then set a hard spend limit on the project: once tracked spend reaches it, OpenAI rejects requests with a 429 error.
What do I do with a leaked Stripe secret key?
On the API keys page of the Stripe Dashboard, open the menu on the key and choose Rotate key with the expiration set to Now, so the old key is deleted at once. Open the key's request logs in Workbench and review recent payments, refunds and customers. Then move your server to a restricted key with an access policy.
How do I rotate an API key without downtime?
Create the new key, deploy it, check that your app uses it, then revoke the old one. Stripe keeps both keys working for up to 7 days when you rotate, which suits a routine rotation. After a leak, keep that window as short as you can, and skip it if the key is being abused.
My API key leaked on GitHub. Is deleting the commit enough?
No. The key stays in forks, clones, cached views and pull requests, and anyone who copied it can still use it. GitHub's own guidance is to revoke or rotate the credential first. GitHub revokes its own tokens pushed to public repositories and Anthropic deactivates Claude keys that GitHub reports, but do not count on that for your other keys.
What happens when an AWS access key is exposed?
Anyone holding it can call AWS with the permissions of the IAM user it belongs to. AWS may attach a quarantine policy to that user and open a support case, but the key is not deleted for you. Deactivate it in IAM, check CloudTrail for calls you did not make, and look for new users, roles or access keys created to keep access.
Do I need to rewrite my git history after rotating the key?
Not always. GitHub notes that a revoked or rotated credential can no longer be used, and that rewriting history may then not be warranted. A rewrite changes commit hashes and cannot reach other people's clones, so keep it for history that holds something you cannot rotate, such as personal data.
Is it a leak if my Supabase anon key or Stripe publishable key is in my JavaScript?
No. Both are designed to be public, and rotating them changes nothing for a stranger. What matters is what they give access to: your row level security policies for Supabase, your server-side code for Stripe. The Supabase anon key guide explains the checks.
Sources
- 1.Removing sensitive data from a repository, GitHub Docs
- 2.API keys and service accounts, OpenAI
- 3.Spend limits, OpenAI
- 4.API key best practices: keeping your keys safe and secure, Anthropic
- 5.API keys, Stripe Docs
- 6.Protecting against compromised API keys, Stripe Support
- 7.What to do if you inadvertently expose an AWS access key, AWS Security Blog
- 8.Rotate a leaked or compromised API key, Supabase Docs
- 9.Token expiration and revocation, GitHub Docs
- 10.About push protection, GitHub Docs
- 11.Secret scanning blocks Anthropic keys by default, adds Salesforce, GitHub Changelog
- 12.Generated URLs, Vercel Docs
- 13.Detecting exposed AWS IAM credentials, Palo Alto Networks Unit 42