A key in the bundle is a billable key anyone can read
If your app calls the Anthropic API from browser code with your key in a client-exposed variable, the Anthropic API key is exposed in the client bundle, and that is the launch risk. The key prefix sk-ant- ships inside the JavaScript that every visitor downloads. Anyone who opens DevTools can read it, copy it, and run requests against your account until you notice the charges or hit a limit.
This is not a theoretical leak. A key that reaches the bundle is observable: it sits in plain text in a file the browser already has. There is no exploit step, no clever attack. The browser needed the key to make the request, so the build put the key where the browser could find it. That is exactly what happened, and it is enough.
- Observable: the string sk-ant- appears in a JavaScript file served from your public URL, or in a request the page makes directly to the Anthropic API host.
- Possible from there: anyone can use that key to call Claude on your billing, exhaust your usage limits, or run their own workload through your account.
- This is a secrets-exposure problem (CWE category: use of hard-coded or client-shipped credentials), distinct from an auth bug. The key works as designed; the mistake is shipping it to a place where it is no longer secret.
Why Claude-powered AI apps fall into this
The Anthropic SDK runs the same way in Node and in the browser. So when you (or an AI coding tool) prototype a Claude feature, calling the API straight from a component just works. The text comes back, the UI renders, the feature looks done. Nothing in that loop fails loudly, which is precisely why the missing server layer stays invisible.
To make it work in the browser, the key has to be readable by browser code. In a Vite app that means a variable like VITE_ANTHROPIC_API_KEY; in Next.js it means a NEXT_PUBLIC_ prefixed variable. Both of those prefixes are a deliberate signal to the bundler: inline this value into the client build. The prefix that makes the key reachable in dev is the same prefix that ships it to every visitor in production.
- The SDK does not refuse to run client-side, so the prototype path and the unsafe path are the same path.
- AI tools generate the fastest working version, which is usually the direct browser call, because adding a server route is extra scaffolding the prompt did not ask for.
- Fast deploy means the public URL exists before anyone asks where the key actually lives. Same root cause as other VITE_ prefix leaks; see the note on VITE_ environment variables in the client bundle.
Check your built app for sk-ant-
You can confirm this on your own app, with no attack and no third party involved. Two reliable checks.
Search the built bundle. Run your production build, then search the output for the key prefix. For a Vite app that is the dist directory; for Next.js it is the .next output and anything served under /_next/static/. Grep the built JavaScript for sk-ant- (for example, search recursively for the literal string sk-ant- across the build output). Do this against your own build, not a live third-party site.
Watch the Network tab. Open your deployed app in DevTools, go to the Network tab, and use the Claude feature. Look at where the request goes.
For the manual side of this, the note on finding an API key in the browser network tab walks through the same inspection step by step.
- Suspicious result: sk-ant- appears anywhere in the built client JavaScript. That key is shipping to the browser. Treat it as exposed.
- Safer result: no match in any client-served file. The key is not in the bundle the browser downloads.
- Suspicious result: the browser makes a request directly to the Anthropic API host, with your key in an authorization or x-api-key header. The key is leaving the browser, so it was in the browser.
- Safer result: the browser calls your own backend route (for example a path under /api/), and that route is what talks to Anthropic. The key never appears in any request the browser makes.
Move the call server-side and rotate the key
The fix is a boundary, not a setting. The browser must never hold the key.
Put the Anthropic call behind a server route or edge function that holds the key in a server-only variable. The browser sends a request to your route; your route, running on the server, reads the key from a non-public environment variable and calls Anthropic; your route returns only the result. The key prefix sk-ant- never enters client code. In Vite terms, that means the key lives in a backend service, not in any VITE_ variable. In Next.js, store it in a plain server environment variable with no NEXT_PUBLIC_ prefix, and call Anthropic only from a route handler or server action.
Then add controls on that proxy, because a server route with no auth is a public LLM endpoint on your bill:
Finally, rotate the exposed key. Any key that ever shipped in a bundle must be treated as compromised, even if you fix the routing afterward. Generate a new key in the Anthropic console, revoke the old one, and put the new key only in server-side configuration.
- Require authentication on the route so only your real users can trigger a Claude call.
- Add rate limiting per user or per IP so one caller cannot drain your usage.
- Cap request size and validate inputs before forwarding them to the model.
Moving the call server-side stops the key from leaking and lets you gate who can spend it. It does not, by itself, protect against an authenticated user abusing the feature, prompt-injection in the content you send to the model, or a server route that forwards anything a caller submits. Auth, rate limiting, and input validation on the proxy are separate jobs from hiding the key.
Where VibeCodeGuard fits, and what it does not prove
VibeCodeGuard's Launch Check scans your public bundle for secret-shaped strings, including the sk-ant- pattern. A match is a direct signal that the app calls an LLM API client-side with a billable key, because that prefix has no reason to be in code the browser downloads. The scan flags the exposure with severity and points you at the fix: rotate the key and move the call server-side.
What it does not do is read your server code or confirm your new proxy is safe. It sees the public surface, so it can tell you a key is shipping in the bundle; it cannot tell you whether your server route has auth, whether your rate limiting holds, or whether the key you rotated to is now stored correctly. Those need code and manual review.
A clean Launch Check means the scanner did not find sk-ant- on the public surface it inspected. It does not prove no key is exposed through a path the scan did not reach, and it does not prove your server-side handling is correct. Treat a flagged key as a launch blocker: rotate it, move the call behind a server route, then rerun the scan from a clean browser session. If you build with OpenAI as well, the same client-side leak applies to that key; see the note on an OpenAI API key exposed in the frontend.
> launch check
Scan the public surface before launch.
Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.