How to Stop AI Coding Tools From Leaking Your API Keys (2026)

Before you read the explanation, run the check. Open your deployed site, hit Ctrl+U for the page source, and search for sk_. Then open DevTools, go to the Sources tab, and search your JavaScript bundles for the same prefix. Anything that comes back starting sk_live_, sk-proj-, or service_role is already public, and rotating it is the only fix that counts.

That takes about thirty seconds. It also tells you which article you're reading: prevention, or incident response.

Here's why it keeps happening. Exposed API keys are the #1 security finding in vibe-coded apps. You ask Cursor to add Stripe payments, it writes working checkout code in under a minute, and your live secret key is sitting in the client bundle. One git push later it's public. AI code generators are optimized to produce code that works, not code that's safe, and that gap is where the keys go.

TL;DR

Keep the key in a file the agent and the browser can't reach, then verify the deployed bundle. Five fixes: write .gitignore before your first prompt, put secrets behind server-side API routes, state the constraint in the prompt itself, run secret scanning in CI, and scan the live app. Ignore files alone are not enough, and Cursor's own docs say so.

How to Hide API Keys From AI Coding Agents

The intuitive answer is to add .env to .gitignore and assume the agent can't see it. That's half right, and the half that's wrong is the half that leaks.

Cursor does read .gitignore, plus a dedicated .cursorignore for blocking file access across Agent, Tab, Inline Edit, and @ mentions. But Cursor's documentation is unusually blunt about the limits: "While Cursor blocks ignored files, complete protection isn't guaranteed due to LLM unpredictability." It goes further, noting that "the terminal and MCP server tools used by Agent cannot block access to code governed by .cursorignore". An agent that can run cat .env in your terminal has read your key regardless of what the ignore file says.

Windsurf works differently again. Cascade reads a separate .codeiumignore file, so a .gitignore entry on its own doesn't stop it from viewing or editing a path.

Bolt and Lovable generate and run your app in the browser. There is no ignore file to write and no local filesystem the agent is being kept out of. Treat every value you paste into those tools as something the client bundle may end up carrying.

So the ordering that actually holds:

  1. The key lives in your host's dashboard (Vercel, Railway, Netlify, Fly) and nowhere else. Not in the repo, not in a file on your laptop that the agent indexes.
  2. Local development uses a throwaway test key, so the worst case of an agent reading .env.local is a leaked sk_test_.
  3. Ignore files are the third layer, not the first.

Prompt patterns that prevent it

The biggest single change is telling the model the constraint up front. Left unprompted it reproduces the tutorial pattern it was trained on, which is a hardcoded key.

AI Prompt

Use this instead of 'add Stripe checkout'

Add Stripe checkout. Read the secret key from process.env.STRIPE_SECRET_KEY, never inline it or use a placeholder string. Put every Stripe call in a server-side API route. The browser component should call my own route and never import the Stripe server SDK. Add STRIPE_SECRET_KEY= to .env.example with an empty value.

AI Prompt

Audit prompt for code the AI already wrote

Search this codebase for any credential that reaches the browser: string literals matching sk_, sk-, service_role, AIza, or xox, plus any process.env read inside a client component or a file with "use client". List each one with its file path and say whether it's a public key or a secret. Do not fix anything yet.

Asking for the audit without the fix matters. Let it fix things in the same pass and it will happily "solve" an exposed key by moving it into a NEXT_PUBLIC_ variable, which ships the exact same string to the exact same browsers.

Why AI Tools Get This Wrong

AI models are trained on millions of code samples: tutorials, Stack Overflow answers, README examples, and open-source projects. Most of those samples use hardcoded keys for simplicity. The AI learns this pattern and reproduces it.

There's no malicious intent. The model simply doesn't distinguish between "demo code" and "production code." When you say "add Stripe payments," it generates the most common pattern it's seen:

What AI often generates (dangerous)
// AI-generated code, looks correct, works immediately
const stripe = new Stripe("sk_live_51abc123...");

const session = await stripe.checkout.sessions.create({
  line_items: [{ price: "price_1234", quantity: 1 }],
  mode: "payment",
  success_url: "https://yourapp.com/success",
});

That sk_live_ key is your Stripe secret key. If this code ends up in a client-side bundle or a public repository, anyone can charge your customers, issue refunds, or access your financial data.

5 Common Exposure Patterns

Pattern 1: Hardcoded Keys in Source Files

The most straightforward mistake. The AI writes a key directly in the code:

Hardcoded key in source
// AI generates this. Key is now in your git history forever
const openai = new OpenAI({ apiKey: "sk-proj-abc123..." });

Even if you replace it later, the key lives in your git history. Anyone with repo access can find it with git log -p.

Pattern 2: Secret Keys in Client-Side Bundles

AI tools often don't distinguish between server and client code. In Next.js, for example:

Secret key accidentally exposed to the browser
// AI puts this in a client component. Key ships to every browser
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.SUPABASE_SERVICE_ROLE_KEY, // This should NEVER be public
);

Any environment variable accessed in client-side code gets bundled into JavaScript that's sent to every user's browser. View Source or browser DevTools is all it takes to extract it.

Pattern 3: .env Files Committed to Git

AI tools generate .env files with real-looking keys but don't always create or update .gitignore:

Generated .env with no .gitignore entry
# AI creates this file
DATABASE_URL=postgresql://user:password@db.example.com:5432/prod
STRIPE_SECRET_KEY=sk_live_51abc...
OPENAI_API_KEY=sk-proj-abc...

If .env isn't in .gitignore, your next git add . commits every secret to your repository.

Pattern 4: Keys in AI Chat Context

When you paste error messages or code snippets into AI tools, you may include keys in the prompt. These can end up in:

  • The AI provider's training data (depending on their data policy)
  • Chat history logs that other team members can access
  • Screenshot-based bug reports

Pattern 5: Overly Permissive Public Keys

AI tools often use full-access keys when scoped, limited keys would suffice:

Over-privileged key usage
// AI uses the admin key when a read-only key would work
const firebase = initializeApp({
  apiKey: "AIza...",
  // AI doesn't set up Security Rules or use restricted keys
});

Real-World Consequences

Exposed keys aren't theoretical risks. Here are real incidents that cost companies millions:

Moltbook: 1.5 million API keys exposed from a vibe-coded app. Moltbook, an AI social network built entirely through vibe coding, launched in January 2026. Within five days, Wiz researchers found a Supabase API key in client-side JavaScript that granted full unauthenticated access to the production database, with no Row Level Security configured. The exposed data included 1.5 million API auth tokens, 35,000 email addresses, and private messages containing plaintext OpenAI, Anthropic, and AWS credentials.

AWS crypto mining: $45K-$89K overnight bills. This happens constantly. One founder woke up to a $45,000 AWS bill after attackers found his keys on GitHub and installed crypto miners on Lambda. Palo Alto Networks documented an organized campaign called EleKtra-Leak that automatically harvests AWS keys from GitHub within 5 minutes of being pushed.

Stripe key exposures enable direct fraud. Truffle Security identified 5 distinct attack paths from a single leaked Stripe secret key, including querying all customer PII, issuing unauthorized refunds, and manipulating pricing. A separate web skimming campaign leveraged Stripe's own API to validate stolen payment cards across 49 merchants.

OpenAI keys stolen via Chrome extensions. A fake Chrome extension called "H-Chat Assistant" harvested over 459 API keys from 10,000+ users. Stolen keys gave attackers access to billing accounts. One case involved a key with a $150,000 usage limit.

The scale: 23.8 million secrets leaked on GitHub in 2024. GitGuardian's annual State of Secrets Sprawl report found the number has quadrupled since 2021. Over 70% of secrets leaked in 2022 were still active two years later, and .env files have a 54% chance of containing at least one secret.

Even the big guys mess up. In 2016, attackers found hardcoded AWS credentials in Uber's private GitHub repos and used them to download personal data of 57 million users. The result: a $148 million settlement with all 50 US states and a criminal conviction for Uber's former CSO. If Uber's engineers can leak keys, so can anyone.

How Each AI Tool Handles Secrets

Not all AI code generators handle secrets equally. Here's how the major tools compare:

Cursor
Use with Caution

Uses your local codebase and .env files. Respects .gitignore and won't send ignored files to the AI, but it can still generate code that hardcodes keys if your prompt doesn't specify otherwise.

Bolt
Risky

Generates full-stack apps entirely in-browser. No built-in secret management, so keys frequently end up in client-side code. You must manually move secrets to environment variables after generation.

Lovable
Risky

Generates and deploys full applications. Has basic environment variable support, but the line between server and client code is blurred. Secrets can easily leak to the client bundle.

GitHub Copilot
Safe to Use

Inline code completion only. Won't autocomplete recognized API key patterns. GitHub's push protection can block commits containing detected secrets.

V0
Safe to Use

Focused on UI component generation. Rarely generates backend or secret-handling code, so exposure risk is minimal.

Replit
Use with Caution

Full IDE with built-in deployment. Has a dedicated Secrets tab for managing environment variables, but the AI assistant may still hardcode keys directly in source files.

Bolt and Lovable deserve extra caution. Because they generate entire applications including backend and frontend together, the boundary between server-side and client-side code is easily blurred. Always review generated code for secret placement before deploying.

5 Strategies to Stop Key Exposure

1

Set Up Environment Variables Before Writing Code

Create your .env.local and .gitignore files before you start prompting the AI. This establishes the pattern the AI will follow:

# Create .gitignore first
echo ".env\n.env.local\n.env.production" >> .gitignore

# Create .env.local with your keys
touch .env.local

# Create .env.example with placeholders (safe to commit)
echo "STRIPE_SECRET_KEY=sk_test_your_key_here\nOPENAI_API_KEY=sk-your_key_here" > .env.example
2

Keep Secret Keys Server-Side Only

Every framework has a convention for what reaches the browser. Know yours:

FrameworkServer-onlyClient-safe (public)
Next.jsprocess.env.SECRETprocess.env.NEXT_PUBLIC_*
Nuxt 3runtimeConfig.secretruntimeConfig.public.*
Vite/VueNot accessible by defaultimport.meta.env.VITE_*
SvelteKit$env/static/private$env/static/public

Tell the AI explicitly. Include "use server-side API route" or "keep this key server-side only" in your prompt. AI tools follow instructions better when security requirements are stated upfront.

3

Use Server-Side API Proxy Routes

Instead of calling third-party APIs directly from the browser, create a server-side route:

Server-side API route (Next.js)
// app/api/generate/route.js runs on the server. Key never reaches the browser
import OpenAI from "openai";

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY, // Server-only, no NEXT_PUBLIC_ prefix
});

export async function POST(request) {
  const { prompt } = await request.json();

  const completion = await openai.chat.completions.create({
    model: "gpt-4",
    messages: [{ role: "user", content: prompt }],
  });

  return Response.json({ result: completion.choices[0].message.content });
}
Client code calls your API route, not OpenAI directly
// components/Generator.jsx is browser code. No secrets here
async function generate(prompt) {
  const res = await fetch("/api/generate", {
    method: "POST",
    body: JSON.stringify({ prompt }),
  });
  return res.json();
}
4

Add Secret Scanning to Your Workflow

Catch leaked keys before they reach production:

GitHub secret scanning setup
# .github/workflows/secret-scan.yml
name: Secret Scanning
on: [push, pull_request]

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Gitleaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Also enable GitHub Push Protection in your repository settings. It blocks pushes that contain detected secrets.

5

Scan Your Deployed Application

Secret scanning catches keys in your source code, but some keys only appear in the built, deployed application: embedded in JavaScript bundles, visible in network requests, or leaked through error pages.

Run a CheckYourVibe scan to detect secrets in your live application. The free tier checks for exposed API keys in client-side bundles, misconfigured environment variables, and publicly accessible configuration endpoints.

API Key Security Checklist

Before Every Deploy

How do I hide API keys from AI coding agents?

Ignore files help, but they're not a guarantee. Cursor reads .gitignore and .cursorignore, yet Cursor's own docs warn that "complete protection isn't guaranteed" and that "the terminal and MCP server tools used by Agent cannot block access to code governed by .cursorignore". Windsurf uses a separate .codeiumignore file, so a .gitignore entry alone doesn't stop Cascade. The reliable version is that the key never sits in a file the agent can reach at all: keep it in your host's dashboard and let only the deployed server read it.

How do I stop AI from putting API keys in code?

State the constraint in the prompt itself. "Read the key from process.env.STRIPE_SECRET_KEY, never inline it, and put the call in a server route" produces the right shape. Without that sentence the model reproduces the tutorial pattern it was trained on, which is a hardcoded key.

Are public keys like Supabase anon keys safe to expose?

Supabase anon keys and Firebase public config are designed to be client-facing when used with Row Level Security (RLS). But secret keys like service_role, sk_live, or any key prefixed with "secret" must never appear in client code.

What should I do if my API key is already exposed?

Rotate the key immediately in the service's dashboard. Don't bother scrubbing it from git history. Assume it's compromised. Check service logs for unauthorized usage, update production environment variables with the new key, and set up monitoring for anomalies.

How do I know if my deployed app has exposed keys?

Run a free CheckYourVibe scan. It checks your deployed application for API keys in client-side JavaScript bundles, exposed environment variables, and other secret leakage patterns.

Is Your App Leaking API Keys?

AI-generated code may have exposed your secrets. Run a free CheckYourVibe scan to find API keys in your client-side bundles, misconfigured environment variables, and more.

Best Practices

How to Stop AI Coding Tools From Leaking Your API Keys (2026)