← All field notes
App Security

How a Stripe Secret Key Ends Up in a Public Repo (And How to Check Yours)

The three real ways sk_live_ keys leak from AI-built apps, plus the 5-minute check to find out if yours already has.

Matthew TurleySeptember 25, 20266 min read

One of the most common ways a Stripe secret key leaks has nothing to do with your code. Someone on the team pastes it into ChatGPT to ask why a webhook isn't firing. The webhook was fine. Now the key sits in a third-party chat log with sk_live_ right there at the front. That's the leak nobody thinks to check for. Everyone's watching for the dramatic one.

I review AI-built apps before launch, Lovable, Bolt, Replit, v0, Base44, Cursor output, all of it, and a Stripe key check is one of the first things I run. It almost never looks like a hack. It looks like a founder moving fast with a tool that doesn't know the difference between a key that's fine in the browser and one that absolutely is not.

sk_live_ versus pk_live_, and why the AI mixes them up

Stripe gives you two keys per environment. The publishable key starts with pk_live_ (or pk_test_ in test mode) and it's meant to sit in your frontend. It can load Stripe.js, collect and tokenize card details in the browser, and confirm a payment your server already set up. That's about it. Stripe designed it assuming the whole internet can read it.

The secret key starts with sk_live_. It can issue refunds, pull customer records, create charges, list every payment your business has ever taken. It belongs on your server, never in anything the browser downloads.

Here's the mechanism that actually causes the leak. An AI coding tool is building your checkout flow, hits a Stripe API call that isn't working client-side, and the fastest path to "the demo works" is grabbing whatever env var has the word STRIPE in it. If your secret key is sitting in a variable named NEXT_PUBLIC_STRIPE_KEY or VITE_STRIPE_SECRET, that NEXT_PUBLIC_ or VITE_ prefix tells the bundler to inline the value straight into the JavaScript every visitor downloads. The AI doesn't know which key is which. It knows the call needs a key, and one is right there. I covered this exact bundler mechanism in more depth in why your API key ends up in the frontend bundle, Stripe is just the most expensive version of it.

The three ways it actually leaks

Bundled into the frontend. Covered above. Ctrl+F your production bundle for sk_live_ and if it's there, your key isn't a secret anymore, it's public information with your whole Stripe account behind it.

Committed to git history. This one's sneakier because the fix people reach for doesn't fix it. Someone hardcodes the key while testing, catches it, deletes the line, commits again. Feels resolved. It isn't. The original commit is still sitting in .git/, and if the repo is public or ever becomes public, git log -p walks right back to it.

Pasted somewhere that isn't your codebase at all. Slack, a support ticket, a ChatGPT session asking for debugging help, a screen-share recording that gets uploaded somewhere. None of these show up in a code scan. All of them are the same leak. This is the one that's genuinely gotten more common since AI coding assistants became normal, because pasting your whole .env file into a chat window to get help feels harmless in the moment.

The 5-minute check

Run this against your own repo right now:

git log -p --all | grep -E "sk_live_[a-zA-Z0-9]{20,}"

That walks every commit in every branch, not just what's currently checked out. If it returns nothing, good, that's one leak path closed. If it returns a key, don't just delete the line and commit again, that key is permanently in history unless you rewrite it, and even then Stripe should treat it as burned.

Then check what's actually running in the browser. Open your live site, view source or check the Network tab, and search the loaded JavaScript for sk_live_. If it's there, same answer: rotate now, fix the env var prefix after.

Two tools worth having in the loop permanently rather than running once: gitleaks is free, open source, and scans a repo's full history for exactly this pattern. GitHub's secret scanning also covers Stripe keys: push a live key to a public repo and GitHub reports it to Stripe, which may notify you before you've noticed. I go through where these tools fit against static analyzers like Semgrep and Snyk in the security scan tools comparison, gitleaks specifically is the one built for this exact job.

If you find one

Rotate it. Stripe dashboard, Developers, API keys, roll the key, and set the old one to expire now rather than on a delay. Killing the old key matters more than however long you spend investigating how it got there.

Then check your webhook handler while you're in there, that's the other half of this problem. If it skips signature verification, anyone who finds your endpoint URL can POST a fake "payment succeeded" event and your app will believe them. It's the same shape as an exposed anon key with no RLS behind it: the check that should gate access never runs.

After rotating, git history is the part people skip. The old key is still readable in every clone of that repo that exists anywhere, including forks you don't control. Rotating in Stripe is what actually neutralizes it, rewriting history is just cleanup.

Why this keeps happening in AI-built apps specifically

None of this is unique to AI tooling. Developers have leaked Stripe keys since Stripe existed. What's changed is the volume and the speed. When I scanned 66 live AI-built apps, 13 of the 32 running on Supabase had a table anyone could read with the public key. Different key, same shape of problem: a tool that optimizes for "the payment went through" has no reason to ask whether the key that made it work belongs in a .env.local file that never leaves the server or in a bundle that ships to every browser. The demo doesn't care. Your Stripe balance does.

If you want someone to go through your whole payment flow before you launch, not just the key but the webhook verification and the checkout session handling around it, that's what a Ship Check is. One option among a few. The free version is the two commands above, run them first.

Worried your app has holes like these?

Drop your email and we will run a free teardown of your stack and send back the leaks we find, no pitch.

M
Matthew Turley, Continuum

Fractional CTO and embedded technical partner. 20+ years shipping production software.

Run a free Leak Check →