Guide · 12 min read

How to write effective .cursorrules

Cursor rules are the single highest-leverage change you can make to your AI coding workflow — and most teams get them wrong. Here's what actually moves the needle after shipping tens of thousands of lines with agent-driven codebases.

What are cursor rules?

Cursor rules are a project-level configuration that Cursor (and other AI-first editors like Windsurf and Claude Code) inject into every model prompt. Historically they lived in a single .cursorrules file at the repo root. Cursor now supports a richer .cursor/rules/ directory with scoped MDC files, but the same principles apply: everything you put in there is unconditionally prepended to the agent's context on every turn.

That's the point — and it's also the trap. Rules are expensive (they burn context on every request) and they compound (a bad rule silently degrades every response). Treat them like production code.

Three anti-patterns that silently degrade agents

1. Vague aspirations instead of enforceable rules

The most common failure mode: rules that read like a mission statement. "Write clean, maintainable code." "Follow best practices." "Be concise." The model already believes it's doing all of that. These rules add tokens and change nothing.

Replace aspirations with rules that describe an observable behavior:

# BAD — aspirational, unenforceable
- Write clean, well-structured code
- Follow SOLID principles
- Prefer readable code

# GOOD — observable, testable
- Never use `any` in TypeScript. If a type is genuinely unknown, use `unknown` and narrow.
- Server functions live in `*.functions.ts`; server-only helpers in `*.server.ts`.
- Route handlers under 40 lines — extract to a helper if longer.

2. Telling the agent what NOT to do without saying what to do instead

Negative rules ("don't use X") leak model behavior. The agent will invent an alternative, often something worse. Every negative rule needs a positive counterpart.

# BAD
- Never use useEffect for data fetching

# GOOD
- Never use useEffect for data fetching. Use TanStack Query with
  `ensureQueryData` in the route loader and `useSuspenseQuery` in the
  component. Example: src/routes/products.$slug.tsx.

3. Silent assumptions the model can't verify

Rules like "use our design system" or "match the existing style" assume the model can see files it hasn't opened. It can't. Either paste the specific tokens into the rule, or point it at a canonical file it must read before writing.

# GOOD — points at a source of truth
- All colors, fonts, and spacing come from `src/styles.css` (HSL tokens
  under @theme). Never hardcode `text-white`, `bg-[#hex]`, or Tailwind
  color utilities. Read src/styles.css before adding any UI component.

The structure that actually works

After a lot of iteration, four sections cover ~95% of what a rules file needs to do. Order matters — the model weights earlier instructions more heavily.

Section 1: Stack identity (2–3 lines)

One sentence naming the framework, runtime, and any non-obvious constraint. This anchors every downstream decision.

# Stack
TanStack Start on Cloudflare Workers (nodejs_compat). React 19, Tailwind v4,
Supabase for auth/DB. No Node-only packages — this is not a Node runtime.

Section 2: Critical rules (5–15 items)

Things that break the app if the model gets them wrong. Each rule is one line, describes an observable behavior, and (where useful) points at an example file. If a rule needs a paragraph, it belongs in a linked doc, not the rules file.

Section 3: Common mistakes (with before/after)

This is where models learn the fastest. For each mistake the model actually makes on your codebase, write a "bad" and a "good" snippet. Three or four of these outperform twenty prose rules.

Section 4: Project conventions (short list)

File locations, naming patterns, "where does X live." Keep this factual: where routes live, where server functions live, where shared UI lives. Do not put opinions here.

Scoping with .cursor/rules/

The single-file .cursorrules approach hits a wall around 300 lines. At that point every request is paying for rules about tRPC while editing a CSS file. Move to .cursor/rules/ and split by concern:

.cursor/rules/
  00-always.mdc            # applies to every file
  10-typescript.mdc        # globs: **/*.{ts,tsx}
  20-react.mdc             # globs: **/*.tsx
  30-server-functions.mdc  # globs: **/*.functions.ts
  40-supabase.mdc          # globs: **/supabase/**, src/integrations/supabase/**
  50-tailwind.mdc          # globs: **/*.tsx, src/styles.css

Each file has YAML frontmatter with an alwaysApply flag and a globs pattern. The agent only loads the rules whose globs match the files it's touching this turn. This single change often cuts prompt size by 60–70% while making each active rule sharper.

How to know your rules are working

Rules are code. Test them the same way. Two techniques that work:

1. The regression file. Keep a scratch file with 10–15 prompts that used to produce wrong output. Every time you change a rule, rerun the prompts. If a prompt regresses, the rule change caused it. This catches the "I added a rule and now unrelated things broke" failure mode.

2. The token diff. Cursor shows the total context length. When you add a rule and don't see the model change behavior, check whether the token count went up. If yes, the rule is loading but ineffective (rewrite it). If no, the rule isn't being loaded (fix the glob).

Starter template

Copy this as .cursor/rules/00-always.mdc and adapt:

---
description: Project-wide rules that apply to every file
alwaysApply: true
---

# Stack
{one sentence: framework, runtime, DB, non-obvious constraint}

# Critical
- {rule 1 — observable behavior + example file}
- {rule 2}
- ...

# Common mistakes
### Data fetching in React components
BAD:
```tsx
useEffect(() => { fetch('/api/x').then(setData) }, [])
```
GOOD:
```tsx
const { data } = useSuspenseQuery(xQueryOptions)
```

# Where things live
- Routes: `src/routes/`
- Server functions: `src/lib/*.functions.ts`
- Shared UI: `src/components/`
- Design tokens: `src/styles.css` (@theme)

Ship faster with a pre-made bundle

If you'd rather start from a battle-tested rule set than write one from scratch, Nightforge ships bundles that encode these patterns for specific stacks — Claude Code, Drizzle, Supabase + Next.js, Hono on Workers, and more. Every bundle is a CLAUDE.md / .cursorrules file with the exact structure above, tuned to the mistakes that stack's agents make most often.

TL;DR

Cursor rules are prompt engineering with compound interest. Rules that describe observable behavior and point at real files change model output. Aspirational rules and negative rules without positive counterparts do not. Scope with .cursor/rules/, test with a regression file, and keep the file short enough that you can read the whole thing in under two minutes.