Cursor's AI assistant can write a functioning API call in seconds. It can also bake your OpenAI key directly into that function if you are not watching closely. The model suggests code based on context it sees in your codebase, and if a key appears anywhere in that context, it may echo it right back into a new file you are about to commit.
This guide covers the three most common Cursor-specific exposure patterns, how to find them, and how to fix the root cause for Next.js and Vite apps.
TL;DR
Cursor API key exposure comes from three sources: Cursor's AI hardcoding a key it saw in your codebase context, a NEXT_PUBLIC_ or VITE_ prefixed variable baking the secret into your client bundle, or a committed .env or .cursorrules file. Rotate the exposed key first, then fix the variable name or move the call to a Route Handler or Netlify Function. Scan generated code for key patterns before every commit.
The Three Cursor-Specific Exposure Patterns
Unlike app builders that deploy your app, Cursor is an IDE. The exposure risks are different.
Pattern 1: AI echoes a key from context. Cursor reads files in your project to provide accurate suggestions. If your .env or a config file is open, the model may suggest that exact key value in a new file.
Pattern 2: Wrong variable prefix. Cursor generates NEXT_PUBLIC_OPENAI_API_KEY or VITE_OPENAI_API_KEY because it has seen those patterns frequently in training data. Both prefixes are designed to put values into the browser bundle.
Pattern 3: Secrets in .cursorrules. Developers sometimes paste API keys into .cursorrules to help Cursor understand their service setup. That file gets committed, and the key is in git history.
Step 1: Find the Exposed Key
Search your codebase for key patterns.
# Common key prefix patterns in Cursor-generated code
grep -rn "sk-proj-\|sk-ant-api\|sk_live_\|rk_live_\|AKIA\|eyJhbGci" . \
--include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" \
--exclude-dir=node_modules
# Also search for generic secret assignment patterns
grep -rn "apiKey.*=.*['\"][A-Za-z0-9_\-]\{20,\}" . \
--include="*.ts" --include="*.tsx" --include="*.js" \
--exclude-dir=node_modules
If any match returns an actual key value (not a process.env. reference), that key is hardcoded.
Check for NEXT_PUBLIC_ and VITE_ prefixed secrets.
# Next.js: any secret-like var with the client-bundle prefix
grep -rn "NEXT_PUBLIC_" . --include="*.ts" --include="*.tsx" --include="*.env*"
# Vite / React: same prefix class
grep -rn "VITE_" . --include="*.ts" --include="*.tsx" --include="*.env*"
Variables like NEXT_PUBLIC_OPENAI_KEY or VITE_STRIPE_SECRET are in every visitor's browser.
Inspect your .cursorrules file.
cat .cursorrules 2>/dev/null || cat .cursor/rules 2>/dev/null
# Also check if it was ever committed
git log --all --oneline -- .cursorrules
If you see an actual key value (not a placeholder like your-key-here), the key is in git history.
Check your deployed JS bundle. Open your deployed app in a browser, press F12, go to Sources, and search for sk-proj-, sk_live_, or AKIA. If a key appears in a .js file, it is in your bundle.
Check git history for committed .env files.
git log --all --full-history -- .env
git log --all --full-history -- .env.local
Any result means the secret was stored in your repo.
CheckYourVibe scans your deployed app for API keys in the JavaScript bundle, including OpenAI, Anthropic, Stripe, Supabase, and AWS credentials. It catches what a visitor's browser can read, not just what is in source.
Step 2: Rotate the Exposed Key Immediately
Rotate before fixing code. The key is live and usable right now.
Generate a new key in the affected service's dashboard (OpenAI, Stripe, Anthropic, Supabase, etc.).
Update your deployment environment variables. In Vercel: Project Settings > Environment Variables. In Netlify: Site Settings > Environment Variables. In Railway: Variables tab.
Redeploy so the new variable takes effect in production.
Revoke the old key. Delete or disable it in the service dashboard. Do not leave it active.
Check usage logs for spikes in the exposure window. OpenAI, Stripe, and AWS all provide per-key usage reports.
If the key was ever in a public GitHub repo, treat it as compromised regardless of exposure time. Automated scanners index public repos within minutes of a push.
Step 3: Remove from Git History (if committed)
# Install git-filter-repo
pip install git-filter-repo
# Remove sensitive files from all history
git filter-repo --path .env --invert-paths
git filter-repo --path .cursorrules --invert-paths # if it contains secrets
After scrubbing, force-push to all remotes and notify collaborators to re-clone. Add the files to .gitignore:
echo ".env" >> .gitignore
echo ".env.local" >> .gitignore
# Only add .cursorrules if it contains secrets
# Otherwise keep it for team context
git add .gitignore && git commit -m "chore: gitignore sensitive files"
Step 4: Fix the Root Cause
Next.js Apps (remove NEXT_PUBLIC_)
Cursor frequently generates NEXT_PUBLIC_OPENAI_API_KEY because it has seen that pattern in training data. Remove the prefix and move the API call server-side.
Wrong (key in browser bundle):
// app/components/Chat.tsx - runs in browser, key visible in bundle
const response = await fetch("https://api.openai.com/v1/chat/completions", {
headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPENAI_API_KEY}` },
});
Right (Route Handler keeps key server-side):
// app/api/chat/route.ts - runs on server only
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export async function POST(req: NextRequest) {
const { message } = await req.json();
const response = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: message }],
});
return NextResponse.json({ reply: response.choices[0].message.content });
}
Your React component calls /api/chat. Your Route Handler calls OpenAI. The key stays on the server.
Vite / React Apps (remove VITE_)
Same pattern, different platform:
Wrong (key in bundle):
// src/components/Chat.tsx
const client = new OpenAI({
apiKey: import.meta.env.VITE_OPENAI_API_KEY,
dangerouslyAllowBrowser: true,
});
Right (Netlify Function or Express route):
// netlify/functions/chat.ts
import OpenAI from "openai";
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export const handler = async (event: any) => {
const { message } = JSON.parse(event.body || "{}");
const response = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: message }],
});
return { statusCode: 200, body: JSON.stringify({ reply: response.choices[0].message.content }) };
};
Environment Variable Naming
| Wrong (client-exposed) | Right (server-only) |
|---|---|
NEXT_PUBLIC_OPENAI_API_KEY | OPENAI_API_KEY |
NEXT_PUBLIC_STRIPE_SECRET_KEY | STRIPE_SECRET_KEY |
VITE_ANTHROPIC_API_KEY | ANTHROPIC_API_KEY |
VITE_STRIPE_SECRET | STRIPE_SECRET_KEY |
Step 5: Fix Your .cursorrules File
If your .cursorrules file contains real API keys, clean it and add a rule to prevent Cursor from generating the pattern again:
# .cursorrules
# Rules for this project
## API Keys and Secrets
- Never hardcode API keys, secrets, or passwords in source files.
- Always use environment variables accessed via process.env on the server.
- Client components must never call third-party APIs directly. Use Route Handlers or server actions.
- NEXT_PUBLIC_ prefix is for non-secret config only (feature flags, public URLs). Never for API keys.
- Variable naming: OPENAI_API_KEY (not NEXT_PUBLIC_OPENAI_API_KEY), STRIPE_SECRET_KEY (not NEXT_PUBLIC_STRIPE_SECRET_KEY).
This gives Cursor explicit guidance so it generates correct patterns in future sessions.
Step 6: Prevent Future Leaks
Pre-commit Hook with Gitleaks
# Install gitleaks (macOS)
brew install gitleaks
# Test your current repo
gitleaks detect --source . --verbose
# Add pre-commit hook
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
gitleaks protect --staged --verbose
if [ $? -ne 0 ]; then
echo "Gitleaks found potential secrets. Commit blocked."
exit 1
fi
EOF
chmod +x .git/hooks/pre-commit
Add a .cursorignore
Like .gitignore, .cursorignore tells Cursor not to read certain files as context. Use it to keep secrets out of Cursor's context window entirely:
# .cursorignore
.env
.env.local
.env.production
.env.*.local
*.pem
*.key
secrets/
When .env is in .cursorignore, Cursor cannot read it and cannot echo your keys into generated code.
After adding .cursorignore, restart Cursor so the index rebuilds without the excluded files.
Why does Cursor generate code with hardcoded API keys?
Cursor's AI generates code based on patterns it has seen and on context from your current codebase. If your codebase contains example keys, or if you pasted a key into a prompt or rules file, the model may echo it back in suggestions. Cursor does not intentionally insert keys, but it can reproduce ones it sees in context. Always scan generated code before committing.
What is a .cursorrules file and can it expose secrets?
A .cursorrules file tells Cursor's AI how to write code for your project. If you paste an API key into it so the AI can reference your exact service, and you commit the file, that key is now in git history. Add .cursorrules to .gitignore if it contains any credentials, or strip the keys and reference them by name only.
Why does NEXT_PUBLIC_OPENAI_API_KEY expose my key?
Next.js bundles any variable prefixed with NEXT_PUBLIC_ into the client-side JavaScript at build time. The prefix is designed to share values with browser code, not to hide them. If Cursor creates a NEXT_PUBLIC_OPENAI_API_KEY variable, that key is in your compiled JS files, readable by any visitor. Move the call to a Route Handler and use a variable without the NEXT_PUBLIC_ prefix.
Do I need to rotate my key if I only exposed it briefly?
Yes. Automated scanners index public GitHub repos within minutes of a push. If the key was in a public repo at any point, assume it was found. Rotate it even if you deleted the commit immediately.
How do I tell Cursor not to hardcode API keys in future?
Add a rule to your .cursorrules file: "Never hardcode API keys, passwords, or secrets in source code. Always use environment variables and access them via process.env on the server side only." You can also use Cursor's @Rules feature to set project-wide guidelines.
Check your Cursor-built deployment for API keys in your JavaScript bundle, exposed environment variables, and security header gaps. Free scan, no signup required.