What a leaked key is
An API key, token or password has leaked when it sits somewhere people you don’t trust can read it. The three common places are a git commit you have pushed, the JavaScript your site sends to the browser, and your logs. A leaked key, also called an exposed secret, keeps working until you turn it off.
So the first step is always the same: turn the key off at the provider and make a new one. Cleaning up your code comes second. Many leaked keys keep working for years. GitGuardian, a company that scans public GitHub for keys, found about 28.6 million new secrets in public commits in 2025, and of the working secrets it found in 2022, 64% still worked four years later.
Who runs into this
Anyone who has put a key where it doesn’t belong, often while trying to get a feature working:
- you pasted a key into a file to make a call work, then pushed the project to GitHub;
- you committed your
.envfile, or added it to.gitignoreafter it was already committed; - your app calls OpenAI, Anthropic or Stripe straight from the browser, so the key is in the page’s JavaScript;
- a debug line prints whole requests or settings to your logs, including the header that carries the key;
- GitHub or the provider warned you that a key was found, or you got a bill you can’t explain.
Why deleting the key is not enough
Deleting the key from your code fixes the next version of your code. It does nothing about the copies that already exist.
- In git. A new commit that removes the key leaves the old commit as it was, and anyone who can see the repository can open an old commit. GitHub’s guide to removing sensitive data says that even rewriting the history can’t remove the key from other people’s copies of the repository or from forks, and that the old commits stay reachable through cached views and pull requests. Making the repository private later doesn’t reach those copies either.
- In the browser. When you build a Next.js app, the value of each
NEXT_PUBLIC_variable your code uses is copied into the JavaScript sent to the browser. Vite does the same forVITE_, and its docs say these variables should not contain API keys. Anyone who opens your site can read that JavaScript. The value is fixed when the app is built, so a key stays in every build you already deployed until you build and deploy again. - In logs. Anyone who can read your logs can read a key written there. OWASP’s Logging Cheat Sheet lists access tokens, passwords, database connection strings and encryption keys among the data that should usually be removed or masked before it is logged. Supabase’s docs add that a key should never travel in a URL or query string, because those are often logged.
What to do, in order
- Turn the old key off. In the provider’s dashboard, turn the key off, also called revoking it, or make a new key and turn the old one off, also called rotating it. Do this before anything else: nothing you change in your code stops a copy someone already has. GitHub’s guide puts this step first, and OWASP’s Secrets Management Cheat Sheet says an exposed key should be revoked at once. If turning the key off would break your live app, make the new key first, switch the app to it, and turn the old one off straight after. When you rotate a Stripe key, the expiry you pick decides how long the old key keeps working; “Now” turns it off at once.
- Put the new key where only your server can read it. Store it as an environment variable on your host or in a secret manager, with no
NEXT_PUBLIC_orVITE_prefix, and never in a file in your repository. If the browser needs the result, have it call your own server, and have the server call the provider. Supabase’s guide asks you to fix the cause of a leak before you rotate. Otherwise the new key can leak the same way. - Check what the old key was used for. Open the provider’s usage or request logs for the time the key was exposed. Stripe shows request logs for each key, and Anthropic points to the Logs and Usage pages of its Console.
- Take the key out of your code. Read it from the environment instead. If git already tracks your
.envfile, adding it to.gitignoredoes nothing for it; the git docs say to rungit rm --cached .envfirst, which stops git tracking the file and keeps it on your disk. Then commit a.env.examplewith the names and no values. - Decide whether to rewrite history. Once the key is turned off, GitHub’s guide says rewriting the history may not be needed. If you still want it gone, GitHub recommends
git-filter-repowith its--sensitive-data-removaloption. It changes commit IDs, everyone with a copy has to update theirs, and a single merge from an old copy can bring the key back. GitHub Support can remove cached views and pull request references afterwards, but only for data whose risk can’t be removed by rotating the key. - Look for other keys. Where one key leaked, check for others. Search your code, your git history, your built front end and your logs with a tool that looks for keys, also called a secret scanner. Here is a small one that shows how they work:
/*
* Look for likely secrets in text before it leaves your machine: a staged
* diff (`git diff --cached`), a file from your built front end (.next/static
* or dist) or a log file.
*
* It knows a few documented key formats, bearer tokens copied into logs, a
* .env file in a commit, and one common mistake: a secret-sounding variable
* with a prefix that copies its value into the browser bundle (NEXT_PUBLIC_
* in Next.js, VITE_ in Vite).
*
* A finding gives the line and the kind of key, never the key itself, so the
* report can't leak it a second time. This is a small example of what a
* scanner looks for; for real coverage, run a full one such as gitleaks.
*/
export type Finding = { line: number; kind: string }
const RULES: Array<[kind: string, pattern: RegExp]> = [
['Stripe secret or restricted key', /\b[rs]k_(?:live|test)_[A-Za-z0-9]{16,}/],
['GitHub token', /\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_\w{30,})/],
['AWS access key ID', /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/],
['Supabase secret key', /\bsb_secret_[\w-]{16,}/],
['Private key', /-----BEGIN (?:[A-Z]+ )*PRIVATE KEY-----/],
['Bearer token', /\bBearer\s+[\w.~+/-]{20,}/i],
// A name that sounds secret, with a prefix that ships its value to the browser.
['Secret name with a public prefix', /\b(?:NEXT_PUBLIC|VITE)_\w*(?:SECRET|PRIVATE|PASSWORD|SERVICE_ROLE)/],
// A diff that adds or changes a .env file. A .env.example with names only is fine.
['.env file in a commit', /^\+\+\+ b\/(?:.+\/)?\.env(?!\.example$)(?:\.[\w.-]+)?$/],
]
export function findSecrets(text: string): Finding[] {
const findings: Finding[] = []
text.split(/\r?\n/).forEach((content, index) => {
for (const [kind, pattern] of RULES) {
if (pattern.test(content)) findings.push({ line: index + 1, kind })
}
})
return findings
}The example checks each line for key formats that providers publish: Stripe secret and restricted keys start with sk_ and rk_, GitHub tokens with prefixes such as ghp_ and github_pat_, AWS access key IDs with AKIA or ASIA, and Supabase secret keys with sb_secret_. It also flags private key blocks, bearer tokens copied into logs, a secret-sounding name with a public prefix, and a .env file in a staged change. It reports the line and the kind of key, never the key itself, so the report can’t leak it again. Run on a diff, a match on a removed line still means the key was in an earlier commit.
Not every key is a secret. Stripe’s publishable key (pk_) is made for front-end code, and Supabase says its publishable key is safe on a web page as long as Row Level Security is on for every table. Supabase’s secret key is the opposite: it skips every Row Level Security policy and must never be in a browser or in source control. The example leaves the public kinds alone. Know which kind each of your keys is.
Why it matters for AI-written code
When a feature fails because a key is missing, the quickest way to make it run is to paste the key into the file, or to give it a public prefix so browser code can read it. A coding agent working toward “it runs” can take that shortcut unless you tell it not to. The feature then works when you test it, and nothing on the screen shows that the key is now public.
GitGuardian’s 2026 report found that public commits co-written by Claude Code leaked secrets at about twice the rate of all public commits on GitHub, and adds that the people using the tool remain a critical factor. So make checking for keys part of what you ask the agent to do, as in the prompt below.
You can also keep an agent away from the keys themselves. In Claude Code, the deny rules Read(./.env) and Read(./.env.*) in its settings stop it reading those files with its own tools and with commands such as cat. They don’t stop a script that opens the file by itself; for that, Claude Code’s docs point to its sandbox.
Ways to keep keys out, and how they compare
.gitignorealone. Keeps new.envfiles out of commits. It doesn’t affect a file git already tracks, and it does nothing about a key typed into a source file.- Hiding the key in browser code. There is no hiding place in the browser: any key that browser code can use is in JavaScript that anyone can read. OpenAI’s API reference says not to expose a key in client-side code such as browsers or apps, and to load it from an environment variable or key management service on the server.
- A server route that holds the key. The browser calls your server, and only your server calls the provider. This is the fix for the browser leak. In Next.js, adding
import 'server-only'to a file that reads a secret makes the build fail if browser code imports that file. - A secret scanner on your machine and in CI. gitleaks can run as a check before each commit, also called a pre-commit hook, or as a GitHub Action, and it can scan the whole history. Its README says it is now feature complete and will get security fixes only.
- GitHub secret scanning and push protection. Secret scanning runs free on public repositories and checks the whole history on all branches, plus issues and pull requests. When it finds a key from a partner provider, GitHub tells that provider, which may revoke it; Anthropic, for example, turns off exposed Claude API keys automatically and emails the owner. Push protection for users is on by default and stops you pushing the secrets it detects to public repositories. For private repositories owned by an organization, these features need GitHub Secret Protection.
- A secret manager. Stripe recommends the secrets vault your hosting platform provides, and environment variables if you can’t use one. OWASP recommends different secrets for development and production. Then a leaked development key can’t reach real data.
- Smaller keys. Stripe’s restricted keys carry only the permissions you give them, and its access policies can limit a key to your own servers. A leaked key that can do less does less damage.
How to keep keys out of your app
- List every key your app uses: what it unlocks, where your code reads it, and where in the provider’s dashboard you would turn it off.
- Search your code for
NEXT_PUBLIC_andVITE_. Every match should be a value you are happy for anyone to read. - Move every call that needs a secret into server code, and have the browser call your server instead. In Next.js, add
import 'server-only'to the files that read secrets. - Check that every
.envfile is in.gitignoreand not tracked:git ls-files .envshould print nothing. Commit a.env.examplewith names only. - Add a secret scanner as a pre-commit hook and in CI over the full history, and turn on push protection.
- Log named fields, not whole requests, headers or settings objects, and never put a key in a URL.
- Build the app and search the browser files it produces (
.next/staticfor Next.js,distfor Vite) for the first few characters of each secret key. Nothing should match.
What to tell your coding agent. Paste this into Claude Code, Codex or Antigravity:
Check this project for exposed secrets. Never print a secret value in your replies; refer to each one by its variable name and file. First, list every API key, token, password and connection string the app uses, where it is read, and whether the provider made it to be public, like a Stripe publishable key. Then move any secret that is written in a source file, read by browser code, or named with a NEXT_PUBLIC_ or VITE_ prefix into a server route that reads it from a server-only environment variable; in a Next.js app, add import 'server-only' to those files. Make sure every .env file is in .gitignore and not tracked by git, and add a .env.example with names but no values. Add gitleaks as a pre-commit hook and as a CI job that scans the full git history. Replace any logging of whole requests, headers, environment or config objects with named fields. Finally, list every key that was ever committed or shipped to the browser, so I can turn it off and make a new one in the provider’s dashboard.
To make it a standing rule instead of a one-off request, put the same text in the instructions file your agent reads at the start of a session: CLAUDE.md for Claude Code, AGENTS.md for Codex, and AGENTS.md, GEMINI.md or a file in .agents/rules/ for Antigravity. ArcLab doesn’t connect to any of these tools; you paste the text yourself.
Learn it in depth
This guide covers what to do on the day a key leaks and the habits that stop the next one. The “Keep API keys out of your code and git history” module in the Security Engineering track starts from an example incident, a key that shipped in a JavaScript bundle and was found through the bill, has you choose what to do first in a scenario, and ends with a secrets inventory and rotation plan you write for your own app.
This guide fixes one problem. ArcLab covers 35.
$29 one-time for all 35 modules, plus the ones we add. Or start free: check your own app in about 15 minutes. 14-day refund, as long as you haven’t completed more than one module.
Sources
The official documentation this guide was checked against:
- Removing sensitive data from a repository, GitHub Docs: revoke or rotate first; clones, forks, cached views and pull requests; git-filter-repo --sensitive-data-removal; side effects; GitHub Support
- About secret scanning, GitHub Docs: free on public repositories; full history, issues and pull requests; partner providers notified; Secret Protection for private organization repositories
- About push protection, GitHub Docs: push protection for users is on by default and blocks secrets pushed to public repositories
- About authentication to GitHub: token formats, GitHub Docs: ghp_, github_pat_, gho_, ghu_, ghs_ and ghr_ prefixes
- How to use environment variables in Next.js, Next.js docs: NEXT_PUBLIC_ values inlined into browser JavaScript at build time and frozen there; other variables server-only
- Server and Client Components: preventing environment poisoning, Next.js docs: import 'server-only' makes a client import a build-time error
- Env Variables and Modes, Vite docs: VITE_ variables are bundled into client code and should not contain API keys
- Build Options: build.outDir, Vite docs: build output goes to dist by default
- output, Next.js docs: the .next/static folder of a build, meant to be served by a CDN
- Using a CDN with Next.js, Next.js docs: JavaScript and CSS are served from /_next/static/
- gitignore, Git documentation: tracked files are not affected; git rm --cached
- git rm (--cached), Git documentation: removes a file from the index only and leaves it on disk
- git ls-files, Git documentation: lists tracked files by default
- Logging Cheat Sheet, OWASP Cheat Sheet Series: tokens, passwords, connection strings and keys should be removed or masked before logging
- Secrets Management Cheat Sheet, OWASP Cheat Sheet Series: immediate revocation of exposed keys; separate development and production secrets
- API keys, Stripe docs: publishable, restricted and secret keys; rotating and expiring keys; request logs per key; access policies; secrets vault
- Understanding API keys, Supabase docs: publishable vs secret keys; Row Level Security; fix the cause before rotating; no keys in URLs or logs
- API key best practices: keeping your keys safe and secure, Claude Help Center, Anthropic: delete a compromised key; automatic deactivation through GitHub secret scanning; Logs and Usage pages
- API reference overview: authentication, OpenAI API docs: no keys in client-side code; load from an environment variable or key management service on the server
- IAM identifiers: unique ID prefixes, AWS IAM User Guide: AKIA and ASIA access key ID prefixes
- Gitleaks, gitleaks on GitHub: pre-commit hook, GitHub Action, history scans; feature complete
- The State of Secrets Sprawl 2026, GitGuardian: new secrets in public commits in 2025; share of 2022 secrets still valid; Claude Code co-authored commits
- Settings files and precedence, Claude Code docs, Anthropic: deny rules for Read(./.env)
- Configure permissions, Claude Code docs, Anthropic: what Read deny rules cover; sandbox for scripts
- How Claude remembers your project, Claude Code docs, Anthropic: CLAUDE.md is read at the start of every session
- AGENTS.md, Codex docs, OpenAI: Codex reads AGENTS.md before doing any work
- Rules, Google Antigravity docs: workspace rules in AGENTS.md, GEMINI.md or .agents/rules/
Last checked 23 October 2026 against the sources above.
Found a mistake? Tell us.