AI-Native Development

The .cursorrules File Is a Start (But Your Spec Should Be Bigger)

Your .cursorrules file is the single most impactful thing you can do for AI coding context. It's also not enough. Here's what a great rules file looks like, where rules run out, and what to add on top.

4
4ge Team
4ge Team

The File Everyone Opens, Nobody Finishes

We've all done it. Opened .cursor/rules/ (or the old .cursorrules in the project root), started typing the team's conventions, gotten through the tech stack and the naming patterns, then stalled out somewhere around "use our existing error handling patterns." The file sits there. It helps. The AI reads it at the start of every session. It's better than nothing — which is what most people had before they created it.

And then there are the people who really committed. The awesome-cursorrules repo has 39,000+ stars. There are hundreds of framework-specific .mdc files — Next.js + Supabase, Angular + Novo Elements, Cloudflare Workers + Hono. Some of them are extraordinary: 27 rules covering security-critical auth patterns, deprecated import warnings, and Row Level Security enforcement. Rules that actually prevent production incidents.

I'm not here to tell you your .cursorrules file is bad. It's probably the single most impactful thing you've done for your AI coding workflow. The problem isn't that rules are weak — it's that they cover one layer of a multi-layer problem, and most developers don't realise where that layer ends.

This article has two halves. The first half is practical: what makes a great rules file, with real examples you can steal. The second half is about what rules can't do — and what you need to build on top of them to stop re-explaining your project every morning.

39,000+

GitHub stars on the awesome-cursorrules repository — the most popular collection of Cursor Project Rules. Rules files aren't a niche practice. They're the default starting point for context engineering.

What Makes a Great .cursorrules File

Before we get to the limitations, let's be brutally specific about what works. I've read hundreds of rules files (the awesome-cursorrules repo is a goldmine) and the difference between a good one and a mediocre one is night and day for your AI's output quality.

The anatomy of a rule that changes AI behaviour

A rule that actually changes what the AI generates has three parts: the instruction, the reason, and the example. Most rules files only include the first one.

Bad rule:

Use the repository pattern for data access.

Good rule:

RULE: Use the repository pattern for data access.

We use repositories (not active record or direct Prisma calls in services).
Each entity gets its own repository class in src/repositories/.
Repositories handle all database operations — services never import Prisma directly.

WHY: We previously used active record and ended up with data access logic
scattered across 40+ service files. The repository pattern centralises this
and makes it testable.

// ✅ CORRECT
const user = await this.userRepo.findById(id)

// ❌ WRONG — direct Prisma access in a service
const user = await prisma.user.findUnique({ where: { id } })

The bad rule tells the AI what to do. The good rule tells it what, why, and what "wrong" looks like. The "why" isn't there for the AI's edification — it's there because when the AI encounters a situation where the rule seems wrong (and it will), the "why" tells it whether the rule still applies or whether this is a valid exception.

Here's a real example from the Next.js 15 + Supabase rules file that gets this exactly right:

// ✅ CORRECT — verified with Supabase auth server
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) redirect('/login')

// ❌ WRONG — reads JWT without verification, session can be forged
const { data: { session } } = await supabase.auth.getSession()

This rule has all three parts: the instruction (use getUser(), not getSession()), the reason (session can be forged), and the example (correct vs wrong). When the AI reads this, it doesn't just follow a convention — it understands a security constraint. That understanding changes behaviour at the edges, not just the center.

Five rules that actually change output quality

If I had to pick the five rules that make the biggest difference in AI output quality — the rules that separate "the AI generates okay code" from "the AI generates code that fits my system":

1. Tech stack declaration with versions and alternatives rejected.

Tech Stack:
- Framework: Next.js 15 (App Router) with React 19 — NOT Pages Router
- Language: TypeScript (strict mode)
- ORM: Prisma — NOT Drizzle, NOT raw SQL
- Auth: Supabase SSR — NOT @supabase/auth-helpers-nextjs (deprecated)
- Validation: Zod — NOT yup, NOT custom validators

The "NOT" clauses are the valuable part. Without them, the AI will happily suggest Drizzle or yup because they're also valid choices. With them, it knows what you rejected — which is just as important as what you chose.

2. File organisation patterns.

Project Structure:
- /src/app/ — Next.js App Router pages and layouts
- /src/components/ — Shared React components (NOT page-specific ones)
- /src/components/ui/ — shadcn/ui primitives (edit via CLI, not manually)
- /src/lib/ — Utilities and helpers (pure functions only, no side effects)
- /src/repositories/ — Data access layer (all Prisma calls live here)
- /src/services/ — Business logic (import from repositories, never Prisma directly)
- /src/middleware/ — Next.js middleware chain (auth before rate limiting)

This tells the AI where to put things and how files relate to each other. Without it, the AI invents its own structure — usually a reasonable one that doesn't match yours.

3. Error handling as an explicit pattern.

Error Handling:
- All API errors use our AppError class (src/lib/errors.ts)
- NEVER throw raw Error objects or use new Error()
- AppError accepts: message, statusCode, errorCode, isOperational
- All service methods return Result<T> (never throw in service layer)
- HTTP layer catches AppError and formats response

// ✅ CORRECT
return err(new AppError('Order not found', 404, 'ORDER_NOT_FOUND', true))

// ❌ WRONG
throw new Error('Order not found')

Error handling is where AI-generated code diverges from your architecture the most. Without a rule, the AI will use whatever error pattern is most common in its training data — which is probably not yours.

4. Import preferences with barrel files.

Import Rules:
- Use barrel files (@/components, @/lib, @/services)
- Absolute paths with @/ prefix — NEVER relative paths like ../../
- Import types with `import type` syntax
- React imports: `import { useState } from 'react'` — not `import React from 'react'`

These seem minor. They're not. Import preferences affect every single file the AI generates. Without them, you'll spend time fixing imports in every review. I've seen PRs that were 60% import rewrites.

5. The thing you tried and rejected.

CRITICAL: We previously used active record with Prisma models that included
business logic. This led to data access smeared across 40+ service files and
took a full sprint to untangle. NEVER put business logic in Prisma models.
ALL data access goes through repository classes.

This is the rule most people skip — but it's the one that prevents the most rework. The AI doesn't just need to know what to do. It needs to know what NOT to do, and why. Otherwise it'll "helpfully" suggest the thing you already tried and rejected, because that thing is also a valid approach — just not for your team, at this moment, given your history.

The .cursorrules audit: 80% of rules are conventions by another name

Go open your .cursor/rules/ directory right now. Read every rule. For each one, ask: is this telling the AI what to do or is it telling the AI why we do it this way?

Most rules files are about 80% "what" and 20% "why." That's not a criticism — conventions are valuable. "Use TypeScript strict mode" is a "what" and it matters. But here's the thing: rules that are pure "what" without "why" are conventions. And conventions are the floor, not the ceiling.

The "why" rules are the ones that actually change AI behaviour at the boundaries:

Conventional rule (what)Contextual rule (why)
"Use the repository pattern""Use the repository pattern because active record led to data access across 40+ files"
"Validate orders first""Validate orders before checking inventory because of the March 15 incident"
"Use Postgres""Use Postgres — our migrations and RLS policies are built for it"
"Server Components first""Default to Server Components because we had a bundle size incident in Q1"

The conventional rules prevent wrong style. The contextual rules prevent wrong architecture. You need both — but most rules files are 95% conventional, 5% contextual.

Where Rules Run Out

Now the uncomfortable part. I've spent 800 words telling you how to write better rules because rules matter. They really do. But there's a ceiling, and if you've been using Cursor for more than a few weeks, you've already hit it.

Rules can't explain why

I keep coming back to this because it's the fundamental limitation. A rule says "use the repository pattern." It doesn't say "we tried active record and it cost us a full sprint of cleanup, and during that sprint we had a production incident because the UserStore was directly querying the orders table."

When the AI encounters a situation where the repository pattern seems like overkill — a simple one-off query in a utility function — it has no information about why the rule exists. It might decide to skip the repository "just this once." And that "just this once" is how you end up back in the mess that made you write the rule in the first place.

The "why" lives outside the rules file. It lives in your team's memory (which degrades) or in your incident documentation (which nobody reads) or nowhere at all (which is how it gets lost).

Rules can't describe component relationships

Your rules file can say "services import from repositories, never from Prisma directly." That's a convention about the relationship between two layers. But what about the relationship between the PaymentService and the InventoryService? What about the webhook handler that the PaymentService triggers after successful checkout? What about the NotificationService that listens for payment failures?

These are component relationships — the data flows, the dependency chains, the "if X happens, Y needs to know about it" connections that make your system a system rather than a pile of independent files. Rules files are flat. They don't have a mechanism for describing these relationships. You can list them as bullet points, but a list of connections doesn't capture the shape of the system — the flows, the branching paths, the failure cascades.

This matters because when the AI generates code for the PaymentService, it doesn't know that a change to the payment retry logic will break the NotificationService listener that expects exactly three retries before alerting. The convention says "services are independent." They're not — and that dependency isn't documented anywhere the AI can find it.

Rules can't capture edge cases

This is the big one. We've covered this before: rules tell the AI the happy path. They don't enumerate the failure paths — the error states, the boundary conditions, the "what happens when" scenarios that become production incidents.

Your rules file says "Use Stripe for payment processing." Does it say what happens when the payment gateway times out? What about when the user clicks "pay" twice? What about when the Stripe webhook is delayed and the order confirmation fires before the payment is verified?

These are edge cases — and they're where AI-generated code breaks most often. Not because the code is bad on the happy path, but because nobody specified the unhappy paths. A rules file doesn't have a mechanism for capturing edge cases. It's a list of conventions, not a map of failure scenarios.

Rules are symmetric — same context for every session

Every session gets the same rules regardless of what you're working on. The rules for the billing module are the same rules for the auth service. The rules for a database migration are the same rules for a UI component update.

This is a design choice, not a bug — rules are supposed to be universal for your project. But it means the AI carries context about your Prisma setup when you're working on a CSS file, and context about your error handling patterns when you're writing a database migration. It's noise. And in a context window with real capacity constraints, noise displaces signal.

Rules don't survive themselves

This is the meta-problem. Your rules file tells the AI your conventions. But who tells the next developer why those conventions exist? Who explains to the new team member that the repository pattern rule exists because of the active record incident? Who documents that the "validate orders first" rule was added after a specific production bug?

The rules file can't carry its own history. It's a flat list of instructions. When the original author of the rules moves on (or simply forgets), the "why" disappears — and the rules become dogma instead of context. "We use the repository pattern because... that's what the rules file says." That's how rules rot: not because they're wrong, but because nobody remembers why they're right.

The Spec Layer: What to Add on Top of Rules

If you've read the Cursor context management article, you've seen the framework: rules are the floor. Specifications are the ceiling. Here's what that looks like in practice — three layers on top of your rules file.

Layer 1: Rules (what you already have)

Your .cursor/rules/*.mdc files. Conventions, tech stack, naming patterns, import preferences, error handling patterns. Keep them. Compress them. The best rules files are focused — twenty good rules beat two hundred mediocre ones. Every rule that the AI has to read costs attention weight. If you have rules that the AI has never followed (check — you might be surprised how many it ignores), remove them.

Layer 2: Architecture Decision Records

One file per significant decision. Stored in /docs/adr/ or wherever your team can find them. Versioned in git alongside your code. Not the decision itself — the context that led to it:

# ADR-007: Validate orders before checking inventory

## Decision
Orders are validated before inventory availability is checked.

## Context
On 2026-03-15, inventory was reserved for an invalid order. The customer was
charged for a product that didn't pass validation. It took three days of manual
refunds to sort out. The root cause was that inventory reservation happened
before order validation, so invalid orders could trigger stock holds.

## Alternative considered
Validate after inventory reservation (better user experience — faster failure
feedback on invalid data). Rejected because the financial risk of charging
for unvalidated orders outweighs the UX improvement.

## Consequence of reversal
Same class of incident will recur. Estimated impact during peak: $2K/day in
manual refunds plus customer trust erosion.

## Author
Sarah Chen, Sprint 14.

This ADR takes maybe 2 minutes to write. And it prevents the AI from "helpfully" optimising the validation order because it seems slow — because the AI can now read the ADR and understand why the order matters.

Rules + ADRs is already a massive improvement over rules alone. The rules tell the AI what to do. The ADRs tell it why. When the "why" is available, the AI makes better decisions at the boundary cases — the situations where the rule seems like it shouldn't apply.

Layer 3: Living Specification

A structured document that describes what your system does, how components connect, what constraints must be maintained, and what the current implementation state is. Not a 50-page PRD — a compressed, task-level specification that the AI can reference at the start of every session.

The specification should include:

  • Component relationships and data flows — not just a list of modules, but how they connect and what happens downstream when something changes
  • Business logic constraints with edge cases — not "validate the order" but "validate the order before checking inventory, because [ADR-007]"
  • Partial implementation state — what's done, what's in progress, what's pending. The AI doesn't need to infer what you're working on if you tell it explicitly.

This is the layer that makes the morning re-introduction tax disappear. Your spec + your rules + your ADRs give the AI the same understanding that you have — architecture decisions, business logic, edge cases, current state. We've written about this at length: the variable isn't model quality, it's context quality. And context quality comes from structure, not volume.

How to Evolve: Rules-Only → Rules + Specs

You don't need to build all three layers in a weekend. Here's the sequence that actually works:

Week 1: Audit your rules file

Open your .cursor/rules/ directory. Read every rule. For each one, categorise it as:

  • Convention (what to do) — keep it in the rules file
  • Decision by stealth (why we do it) — move it to an ADR

You'll find that about 20% of your rules are actually decisions wearing convention clothes. "Use the repository pattern" is a convention. "Use the repository pattern because active record led to data access scattered across 40+ files" is a decision. Move the decisions to ADRs and keep the conventions as rules.

Week 2: Write your first five ADRs

Don't try to document every architectural decision at once. Start with the five most important ones — the decisions that, if the AI got them wrong, would cause the most damage. For most projects, these are: data access patterns, auth flow, payment processing, error handling strategy, and the thing you tried and rejected (because the AI will suggest it otherwise).

Week 3: Build a one-page project specification

Not a 50-page document. One page. The most critical information: component relationships, business logic constraints, current implementation state. Written for the AI, not for humans — bullet points and explicit dependencies beat paragraphs. Update it when architecture changes. Version it in git alongside your code.

Week 4+: Make specs part of the workflow

When a PR changes the architecture, it should also update the spec. When a new ADR is written, it should be linked from the relevant rules file. When the codebase drifts from the spec, the spec should be updated from the codebase — not from someone's memory.

This isn't extra work. It's work you're already doing — re-explaining your project every morning — moved to a structured format that the AI can read at the start of every session instead of you typing it again.

The 2,000-Token Test

Here's a practical way to test whether your context layer is working. Take your current rules file, your ADRs (if you have them), and your project spec (if you have one). Combine them. How many tokens is the total?

If it's under 2,000 tokens, you probably have a good rules file but no contextual layer. Your AI knows your conventions but doesn't understand your architecture.

If it's between 2,000 and 10,000 tokens, you likely have rules + some ADRs. The AI knows your conventions and some of your "whys." It'll make better decisions at the boundary cases — but it still won't know about edge cases or component relationships unless you tell it every session.

If it's between 10,000 and 50,000 tokens, you have a full context layer. The AI starts every session with conventions + decisions + system understanding. It generates code that fits your architecture — not just code that works. That's the target state.

The 2,000-token test isn't about token count for its own sake. It's about signal density. The context window research is clear: 2,000 tokens of structured context that includes file paths, imports, and architecture decisions beats 50,000 tokens of prose that the AI has to parse and interpret. Maximum signal per token. The spec is the compression layer; rules are the quick reference. You need both.

The Honest Take

Your .cursorrules file is good. It's the right starting point. It's the single most impactful thing you can do today to improve your AI coding output. If you haven't created one yet, stop reading this and go do it — start with the five rules I listed above and iterate from there.

But rules are a floor, not a ceiling. They capture what — conventions, patterns, tech stack declarations. They don't capture why — the architectural decisions, the business logic constraints, the edge cases from production incidents, the component relationships that make your system a system instead of a pile of files.

The spec layer on top of rules — ADRs for the "why," a living specification for the system understanding, edge cases surfaced and documented — is what transforms "AI generates code that works" into "AI generates code that fits." Same model. Same developer. The difference is context.

Your rules file is a good start. Your spec should be bigger.


4ge is a context engineering platform — a visual workspace where your architecture decisions, business logic constraints, and edge cases are first-class citizens in every specification. See how 4ge makes specs that rules files can't →

Related: Cursor Context Management: Stop Re-Explaining Your Project · The Complete Guide to Context Engineering · Context Windows Explained: Why Specs Outlive Sessions

Ready to put these insights into practice?

Stop wrestling with prompts. Guide your AI assistant with precision using 4ge.

Get Early Access

Early access • Shape the product • First to forge with AI