← All posts
·9 min read

.cursorrules examples that actually work — a practical guide

Real .cursorrules examples for Next.js, Python, and Rails. Learn how to write Cursor AI rules that reduce hallucinations, enforce your patterns, and cut revision cycles.

Every Cursor user discovers .cursorrules the same way: an agent writes something obviously wrong, you wonder how it got there, and someone in a Discord server says "just put it in your .cursorrules file."

What nobody explains is how to write rules that actually stick. A bad .cursorrules file is worse than no file at all — it gives you false confidence while the agent continues to hallucinate your patterns.

This is a practical guide with real examples. Not theory — actual rules that change agent behavior.

What .cursorrules actually does

Cursor injects your .cursorrules file into the system prompt before every request. That means the agent reads it at the start of every conversation, every inline completion, every Cmd+K invocation.

The practical implication: rules must be compact enough to fit in context and specific enough to change behavior. A 3,000-word .cursorrules file with vague prose won't help you. A 400-word file with precise, code-backed patterns will.

What to put in your .cursorrules file

1. The one-sentence project identity

This sounds like a documentation exercise. It isn't. When the agent knows what the project does, it can fill in gaps in your instructions with the right defaults.

# Bad
This is a web app.

# Good
This is a B2B SaaS application where teams track software requirements.
Users are product managers. The primary output is structured spec documents
consumed by AI coding agents.

The difference: with the second version, the agent knows that "save a spec" means persisting to a database, that error messages should be non-technical, and that the output format needs to be parseable by another AI. Without that context, it guesses — and guesses wrong.

2. Stack + the footguns

List your tech stack. More importantly: list the footguns — the places where the agent's training data leads it to produce code that looks right but breaks your project.

## Stack
- Next.js 15 with App Router (not Pages Router)
- React 19
- TypeScript strict mode
- Tailwind CSS v4
- Supabase (PostgreSQL + Auth)
- pnpm (not npm, not yarn)

## Critical constraints
- App Router only — never use Pages Router patterns or getServerSideProps
- useSearchParams() requires a Suspense boundary — always wrap the parent
- All Supabase queries must include .eq('user_id', userId) — RLS is enabled
- No `any` in TypeScript — use `unknown` and narrow the type

The "critical constraints" block is the heart of your .cursorrules. Each line is a distilled correction — a mistake the agent made that you're preventing from ever happening again.

3. Patterns in code, not prose

This is the most important rule about writing rules: show patterns in code, not descriptions in prose. Agents are code models. They internalize code examples far more reliably than natural language descriptions.

Instead of:

Use the repository pattern for data access.

Write this:

## Data access pattern

Always use this pattern for server-side data access:

```typescript
// ✓ CORRECT
import { createClient } from "@/lib/supabase/server";

export async function getDocument(id: string, userId: string) {
  const supabase = await createClient();
  const { data, error } = await supabase
    .from("documents")
    .select("*")
    .eq("id", id)
    .eq("user_id", userId)
    .single();
  if (error) throw error;
  return rowToDocument(data);
}

// ✗ WRONG — never import from supabase/client in server components
import { createClient } from "@/lib/supabase/client";
```

The ✓/✗ pattern is especially powerful. Most hallucinations happen because the agent is confident about a reasonable-looking-but-wrong approach. Showing the wrong pattern explicitly breaks that confidence.

4. The out-of-scope list

When an agent encounters a spec that doesn't cover something, it implements what seems reasonable to it. The result is usually code you didn't ask for that's now in your codebase and needs to be removed.

An explicit out-of-scope list tells the agent to stop and flag rather than implement:

## Out of scope — ask before implementing
- Email notifications (no email service configured)
- OAuth integrations with Slack, Linear, or Jira (env vars not set)
- Any changes to Stripe or payment flows (locked)
- New database tables or migrations (requires manual review)
- Tests for components that use browser APIs (jsdom limitations)

The difference in practice: without this list, if you ask the agent to "add a user notification when a comment is posted," it will write an email notification function. With the list, it will say "email isn't configured — I'll implement the in-app notification and flag the email part."


Real examples by stack

Next.js + TypeScript

The most common stack. The most common footguns.

# Project
[Project description — what it does, who uses it]

## Stack
- Next.js 15, React 19, TypeScript strict, Tailwind CSS 4
- Supabase (auth + database), pnpm

## Critical constraints
- App Router only — never use getServerSideProps, getStaticProps, or any Pages Router pattern
- Client components must start with "use client"
- useSearchParams(), usePathname() require a Suspense boundary
- No `any` type — use `unknown` and narrow
- All DB queries must include user_id filter

## Server component data pattern
```typescript
// ✓ CORRECT — server component
import { createClient } from "@/lib/supabase/server";

async function getData(userId: string) {
  const supabase = await createClient();
  const { data } = await supabase.from("items").select("*").eq("user_id", userId);
  return data;
}
```

## API route pattern
```typescript
// ✓ CORRECT
import { getAuthenticatedUser } from "@/lib/auth";
import { NextResponse } from "next/server";

export async function POST(req: Request) {
  const user = await getAuthenticatedUser();
  if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  // ...
}
```

## Out of scope
- Email (no service configured)
- Payment flow changes

Python + FastAPI

# Project
[Description]

## Stack
- Python 3.12, FastAPI, SQLAlchemy 2.0, Pydantic v2
- PostgreSQL, Alembic, uv (not pip, not poetry)

## Critical constraints
- Pydantic v2 — model_validator not validator, model_dump not dict()
- SQLAlchemy 2.0 — use select() statements, not legacy query API
- All endpoints require Depends(get_current_user) — no public endpoints without explicit opt-in
- Use async/await throughout — no sync DB calls
- Type hints required on all functions

## DB query pattern
```python
# ✓ CORRECT
from sqlalchemy import select

async def get_item(db: AsyncSession, item_id: int, user_id: int):
    result = await db.execute(
        select(Item).where(Item.id == item_id, Item.user_id == user_id)
    )
    return result.scalar_one_or_none()

# ✗ WRONG — legacy API
db.query(Item).filter(Item.id == item_id).first()

Out of scope

  • Celery/background jobs (not configured)
  • Email sending
  • New DB columns without Alembic migration

### Rails + Hotwire

```text
# Project
[Description]

## Stack
- Rails 7.2, Ruby 3.3, PostgreSQL, Hotwire (Turbo + Stimulus)
- Tailwind CSS, importmap (no webpack/esbuild)

## Critical constraints
- Turbo Frames and Turbo Streams for dynamic updates — no full-page reloads
- Stimulus for JS behavior — no jQuery, no vanilla JS in views
- Strong parameters required on every controller action that accepts params
- before_action :authenticate_user! on every controller
- N+1 queries are not acceptable — always use includes/preload

## Controller pattern
```ruby
# ✓ CORRECT
class ItemsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_item, only: [:show, :edit, :update, :destroy]

  def create
    @item = current_user.items.build(item_params)
    if @item.save
      redirect_to @item, notice: "Created."
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def set_item
    @item = current_user.items.find(params[:id])
  end

  def item_params
    params.require(:item).permit(:title, :body)
  end
end

Out of scope

  • ActionMailer (not configured)
  • Background jobs without confirmation
  • New gems without discussion

---

## The mistakes most .cursorrules files make

**Too much prose, not enough code.** The agent reads your file but behavioral change requires code patterns, not explanations. If your rules read like a style guide, convert the important ones to ✓/✗ code examples.

**Rules that are aspirational, not enforceable.** "Write clean, maintainable code" means nothing. "Functions must be under 40 lines — extract helpers if longer" is specific enough to change behavior.

**Rules that fight the framework.** If you're using Rails and you tell the agent "always use service objects for business logic," you're asking it to add an abstraction that Rails doesn't have a natural home for. Your rules should work *with* your framework's conventions, not against them.

**No footguns section.** This is the highest-leverage thing in your file. Every project has 3-5 places where the agent's default behavior is wrong for your specific setup. Write them down. They're worth 10x the generic best-practice rules.

**Setting it and forgetting it.** A `.cursorrules` file written in week 1 and never updated stops being useful. The maintenance rule: every time you correct the agent, update the file. Three weeks in, the file becomes a distilled record of your project's idiosyncrasies.

---

## The maintenance loop

The pattern that turns a mediocre `.cursorrules` into a great one:

1. Agent produces something wrong
2. You correct it
3. Ask: *"What didn't it know?"*
4. Add that missing knowledge as a concrete constraint or pattern example
5. Never make that correction again

After a month of this, your `.cursorrules` file has captured all the non-obvious facts about your project. The agent's first-pass quality improves noticeably.

---

## .cursorrules vs CLAUDE.md vs AGENTS.md

If you use multiple AI coding tools:

- `.cursorrules` — read by Cursor; keep it focused on Cursor behavior and your stack
- `CLAUDE.md` — read by Claude Code; same pattern, can overlap significantly
- `AGENTS.md` — supported by some frameworks for directory-scoped rules

The principles are identical across all three. The only differences are naming and which tool reads which file. If you write a great `.cursorrules`, you have most of what you need for a `CLAUDE.md` too — copy it, adjust for tool-specific syntax, done.

See [How to write a CLAUDE.md that actually controls your AI coding agent](/blog/how-to-write-claude-md-agents-md) for the Claude Code equivalent.

---

## From rules to per-feature specs

A well-written `.cursorrules` file sets the *environment* — the patterns, constraints, and conventions that apply to every task. It doesn't replace a *feature spec* — the precise description of what you're building right now.

When the agent gets both:
- `.cursorrules` answers: *How do we build things here?*
- A feature spec answers: *What specifically are we building in this session?*

The combination is where the quality jump happens. The agent knows the patterns and knows the destination — the first draft is usually close to final.

[ClearSpec](/) generates per-feature specs in the format that works best as agent input. Describe what you're building, and ClearSpec produces a structured spec you can drop directly into Cursor. Pair it with a solid `.cursorrules` and the revision cycle shrinks to almost nothing.

---

## A minimal .cursorrules template

```text
# [Project Name]

[One sentence: what the project does and who uses it.]

## Stack
- [Framework + version — flag breaking changes]
- [Database]
- [Auth]
- [Package manager]

## Critical constraints (the footguns)
- [Constraint 1 — the mistake the agent keeps making]
- [Constraint 2]
- [Constraint 3]

## [Key pattern name]
```[language]
// ✓ CORRECT
[real example from your codebase]

// ✗ WRONG
[the thing it does without this rule]

Out of scope — ask before implementing

  • [Thing 1]
  • [Thing 2]

Commands

  • Dev: [command]
  • Build: [command]
  • Test: [command]

Fill in the sections. Commit it. Update it every time you correct the agent. That's the whole system.

---

*Using Cursor or Claude Code and want a feature spec that works out of the box? [ClearSpec](/app/specs/new) generates structured specs in under two minutes — drop them straight into your AI coding agent.*

*Further reading:*
- *[How to write a CLAUDE.md that actually controls your AI coding agent](/blog/how-to-write-claude-md-agents-md)*
- *[How to write a spec for Claude Code — a field guide](/blog/how-to-write-a-spec-for-claude-code)*
- *[The product spec template AI agents actually follow](/blog/product-spec-template-ai-agents)*