You built your app with Cursor. It's fast. The code is clean. Everything works.
But there's a problem you probably haven't noticed: Cursor (and every AI code assistant) has a habit of putting your API keys directly in your source code.
Not on purpose. Not maliciously. It's just that when the AI needs to call an API, the simplest thing is to reference the key right there in the code. And since it "works," neither you nor the AI thinks to move it.
Why This Is Dangerous
Hardcoded secrets are the #1 security issue in vibe-coded apps. Here's why:
-
Anyone who sees your code has your keys. If your repo is public on GitHub, your keys are public. If it's private, any collaborator has access.
-
GitHub is constantly scanned. Automated tools crawl public repositories looking for API keys, tokens, and passwords. They find thousands every day.
-
Keys in Git history are permanent. Even if you remove the key from your code and commit, it's still in the Git history. Anyone who clones the repo can find it.
-
The consequences are real. A stolen Stripe key means surprise charges. A stolen AWS key means someone spinning up expensive instances. A stolen database key means all your user data is exposed.
The Most Common Patterns
Here are the specific patterns we see over and over in Cursor-built apps:
Stripe Keys
// Cursor often does this:
const stripe = new Stripe('sk_live_abc123...')
// Instead of this:
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
OpenAI Keys
// AI assistants love to put this directly in the code:
const openai = new OpenAI({ apiKey: 'sk-proj-abc123...' })
// The safe way:
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
Supabase Service Role Keys
// This is the most dangerous one — service role keys bypass all RLS:
const supabase = createClient(url, 'eyJhbGciOi...')
// Never put service role keys in client code. Use the anon key for client:
const supabase = createClient(url, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)
Database Connection Strings
# Cursor often hardcodes these:
DATABASE_URL = "postgresql://user:password@host:5432/db"
# Use environment variables instead:
DATABASE_URL = os.environ.get("DATABASE_URL")
Generic Pattern: Variable Assignment
// Any of these patterns is a red flag:
const API_KEY = "abc123..."
const SECRET_KEY = "sk-..."
const password = "mypassword123"
const db_password = "admin"
How to Find Them
You have three options:
Option 1: Manual Search (Slow but Thorough)
Open your codebase and search for these patterns:
sk_live_,sk_test_(Stripe)sk-followed by long strings (OpenAI)AKIA(AWS Access Key IDs)AIza(Google API keys)ghp_(GitHub tokens)eyJ(JWT tokens)- Any variable named
api_key,secret_key,password,tokenwith a string value
Option 2: Git History Check
Check if secrets were ever committed:
git log --all -p | grep -i "sk_live\|sk_test\|AKIA\|AIza\|ghp_\|password.*="
Option 3: Use Asenvra (Fast and Complete)
Asenvra's ShipCheck scanner runs 19 checks on your codebase, including a comprehensive hardcoded secret detection that covers:
- OpenAI-style API keys
- Stripe live and test keys
- AWS Access Key IDs
- Google API keys
- GitHub personal access tokens
- JWT tokens
- Supabase service role keys
- Generic variable assignment patterns
It finds them using both regex scanning and AST analysis — so it catches patterns that simple text search misses.
What to Do When You Find One
-
Rotate the key immediately. Generate a new key in the provider's dashboard. The old key is compromised — even if you remove it from code, it's still in Git history.
-
Move it to an environment variable. Create a
.envfile (never commit this to Git) and reference it withprocess.env.YOUR_KEY_NAME. -
Add it to
.env.example. List the variable name (not the value) so other developers know they need to set it. -
Add
.envto.gitignore. Make sure.env,.env.local, and.env*.localare all in your.gitignore. -
Clean Git history if needed. If the key was ever committed, use
git filter-branchor BFG Repo-Cleaner to remove it from history. Then rotate the key.
The Cursor-Specific Problem
Here's why this is especially common in Cursor-built apps:
-
Cursor optimizes for "it works now." When you ask it to add Stripe, it puts the key inline because that's the fastest path to a working implementation.
-
Cursor doesn't know about your deployment setup. It doesn't know whether you're using Vercel env vars, Railway env vars, or a local
.envfile. So it uses the simplest approach: put it in the code. -
Cursor generates code in isolation. Each file is generated independently. Cursor might create a Stripe integration in one file, an auth system in another, and neither knows the other exists. There's no global "move all secrets to env vars" step.
Preventing It Going Forward
-
Set up your
.envfile before you start coding. Add all the keys you know you'll need (Stripe, OpenAI, Supabase, etc.) before asking Cursor to implement anything. -
Add a rule to Cursor's instructions. Tell it: "Never put API keys, secrets, or tokens directly in source code. Always use process.env.VARIABLE_NAME."
-
Run Asenvra before every deploy. ShipCheck catches hardcoded secrets, mock data, broken configs, and 16 other issues that AI assistants leave behind.
-
Use
.env.exampleas documentation. Keep it updated as you add new integrations. It's a living document of what your app needs.
The Bottom Line
If you built your app with Cursor (or any AI assistant), there is a real chance you have hardcoded secrets in plain sight. The AI put them there because it was the fastest way to make things work. But "fastest to make it work" and "safe to ship to production" are very different things.
Don't ship with hardcoded secrets. It takes minutes to move them to environment variables. It takes months to recover from a compromised key.