· Vibe coding ·Cursor ·Security

Vibe coding with Cursor — the patterns Cursor keeps shipping

Cursor is fast and capable. It's also the AI editor most likely to ship secrets to your bundle. Here's the .cursorrules file we use to stop that.

Cursor is the AI editor that gets used most for “build something from scratch fast.” It’s the closest thing to Lovable in capability, with the freedom of a real editor underneath. We use it for prototypes. We also see more security holes in Cursor-built apps than any other tool — not because Cursor is bad, but because Cursor’s agent mode optimizes for “make the error go away,” which is the path of least resistance, which is rarely the path of “store the secret server-side.”

This post is the patterns we see, and the .cursorrules file that fixes most of them.

Why Cursor is different from Claude Code

Both ship code. The difference in failure mode:

  • Claude Code is conversational. It plans first, then writes, then summarises what it changed. When it does something risky it usually tells you. The system prompt approach we recommend for Claude works because Claude reads and respects instructions.
  • Cursor (agent mode) is iterative. It writes, runs, sees the error, fixes the error, repeats until the error goes away. When the error is “this env var doesn’t exist,” the fix Cursor reaches for is “use a different env var that does exist” — sometimes the wrong one. When the error is “RLS denied this read,” the fix is “disable RLS.” It’s optimising the loop, not the outcome.

This isn’t a bug. It’s why Cursor is so productive. You just have to set the boundaries before it starts iterating.

The patterns we see most often

1. Service role key smuggled into the client

Pattern: you ask Cursor “make this API call work.” It tries with the anon key, gets RLS-denied, and “fixes” it by using the service role key from the client. The agent loop is happy because the error went away. Your service role key is now in the browser bundle, which means anyone with devtools has root access to your entire database.

This is the single most common Cursor pattern we see.

2. RLS disabled to make the loop terminate

Adjacent pattern: instead of stealing the service role key, Cursor “fixes” the RLS-denied error by running a migration that disables RLS on the table. The error stops. Your table is now public.

If you look at the migration file Cursor wrote, you’ll see something like ALTER TABLE public.users DISABLE ROW LEVEL SECURITY;. It’s right there in the diff if you read it. Most people don’t read the migration.

3. CORS set to * to make the API call work

Pattern: API call fails because the backend doesn’t allow your frontend’s origin. Cursor “fixes” it by setting Access-Control-Allow-Origin: * on the backend. The call works. So does everyone else’s call from anywhere on the internet, with your auth tokens if they can phish them.

4. Auth checks added in the React component, not the API route

Pattern: you ask for “an admin page only admins can see.” Cursor writes {user.role === 'admin' && <AdminPanel />}. The data still loads server-side regardless of the user’s role. Anyone can hit the API directly with curl and get the JSON.

5. .env files generated and committed

Pattern: Cursor needs an env var that doesn’t exist. It generates a .env.local with a placeholder, writes the code, and then — depending on your repo settings — that file ends up in git. Sometimes with the placeholder; sometimes with a real value Cursor pulled from elsewhere in the project.

The .cursorrules file we use

Drop this in your repo root. It pre-empts most of the patterns above:

# Project security rules — apply to every change.

# Secrets and env vars
- Never use a NEXT_PUBLIC_, VITE_, or PUBLIC_ prefix for any secret value (API keys, service role keys, database URLs with credentials, signing secrets, OAuth client secrets).
- Supabase: the anon key is safe in the browser. The service role key is never imported in any file that runs in the browser, even temporarily, even to "make the error go away."
- Never write API keys as string literals. Read from env vars only.
- Never create or modify .env files without explicit instruction. If a .env file is needed, ask first.

# Database (Supabase / Postgres)
- Never disable Row-Level Security on an existing table without explicit instruction.
- Every new table is created with RLS enabled in the same migration.
- "RLS-denied" errors are not fixed by disabling RLS. They are fixed by adding the right policy, or by using a server-side service-role call wrapped in an authorization check.

# CORS
- Never set Access-Control-Allow-Origin to "*" on a route that handles auth or user data. If a CORS error needs fixing, ask which origins should be allowed.

# Authorization
- For every protected route, the authorization check goes server-side, before any data is fetched.
- UI hiding (return null, return <NotAuthorized />) is not authorization. The check has to be at the API or RPC layer.

# General
- When fixing an error, prefer the root cause over the path that makes the error go away. If those diverge, stop and ask.
- Before running a database migration that DROPs or DISABLEs something, stop and confirm.

This file alone catches most of the failure modes we see in Cursor-built apps. The trick is that it has to be in the repo before you start the agent loop, not after. Cursor reads it on session start and uses it as context throughout.

Agent mode vs. chat mode

If you’ve used Cursor for a while, you’ve probably found that chat mode (where you ask, it suggests, you accept) tends to ship cleaner code than agent mode (where you describe the task and it goes off and does it). That matches our experience.

Agent mode is the productivity multiplier. Chat mode is the safety multiplier. We use chat mode for anything that touches auth, payments, or the database, even when agent mode would be faster. The minutes you save aren’t worth the hours of incident response.

What the rules file doesn’t catch

Even with the rules file in place, Cursor will occasionally:

  • Leave debugging code in (console.log(token)) that ships to production.
  • Write tests that pass because they assert against the wrong thing.
  • “Fix” a flaky test by removing the assertion that was catching a real bug.

These aren’t security holes per se, but they’re the kind of thing that becomes one a few iterations later. The pattern of “what was actually shipped” being different from “what you intended to ship” is the core problem Patchable solves. We scan your live app and tell you what’s actually exposed — which is the only check the agent loop doesn’t optimize for.

Cursor is the fastest tool to ship from. Patchable is the fastest way to know what you shipped. Run the scan.

Have us scan your app.

We're taking on a small number of scans right now and running each one personally. Drop your app below — we'll take a look and reach out if it's a fit, usually within a day.

Built by Ahmet & Leonardo in Tallinn. We answer every email to hi@patchable.dev.