Overview

A Cursor project rule is a persistent instruction that Cursor adds to the model’s context for work in a project. Store rules as .mdc files in .cursor/rules/, which are version-controlled with the code. This guide writes them for a TypeScript project; the principles match system-prompts, and the Claude Code equivalent is write-claude-md-from-scratch.

Prerequisites

  • A current Cursor release and a TypeScript project.
  • Knowledge of the conventions the team actually enforces in review. Rules nobody checks drift.

Steps

1. Create the folder and choose an activation mode

mkdir -p .cursor/rules && touch .cursor/rules/project-conventions.mdc

Cursor has four modes: Always Apply (every chat), Apply Intelligently (the agent loads it when the description matches the task), Apply to Specific Files (when open files match globs), and Apply Manually (when you @-mention it). Plain .md files in the folder are ignored, except AGENTS.md, which Cursor also reads as a simpler, frontmatter-free alternative.

2. Write the frontmatter

---
description: Conventions for the storefront's TypeScript code
globs: "**/*.ts,**/*.tsx"
alwaysApply: false
---

Use alwaysApply: true only for rules needed in every conversation, such as architecture or voice; each one spends context on every request. Make description specific enough that intelligent matching can decide when the rule applies.

3. State the project and the stack

Next.js storefront in TypeScript. App Router, Tailwind, Prisma, Postgres.

“This is a web project” carries no information.

4. Write rules that are checkable

TypeScript:
- Strict mode is on. Use `unknown` and narrow; do not use `any`.
- Validate input at API boundaries with zod.
- Avoid `as` casts; if one is required, add a comment saying why.

Files:
- Components live in `src/components/<Name>/index.tsx`; one component per file.
- Database queries live in `src/lib/db/<entity>.ts`.

Tests:
- Vitest, not Jest. Tests sit beside the code as `foo.test.ts`.
- Test behavior, not private methods.

Concrete paths and tool names stop the agent from guessing. Prefer what a linter cannot enforce, what generated code gets wrong repeatedly, and what is specific to this project. Replace “write clean code” with a measurable rule.

5. Add one good-versus-avoid example

// Good: validated at the boundary
const schema = z.object({ email: z.string().email() })
export async function POST(req: Request) {
  const parsed = schema.safeParse(await req.json())
  if (!parsed.success) return Response.json({ error: parsed.error.flatten() }, { status: 400 })
}
// Avoid: unvalidated input
export async function POST(req: Request) { const { email } = await req.json() }

6. Keep each file short

Cursor’s guidance is to keep a rule under 500 lines and to split larger ones into focused files (typescript.mdc, testing.mdc). Reference files instead of pasting their contents, and do not restate a style guide the linter already enforces.

Verify it worked

  1. Open a new chat in Agent mode and ask “How should I add a new API route here?”
  2. The answer should name the folder layout and zod validation from the rules.
  3. Ask for a test of a utility; it should use Vitest and place the file beside the code.

Common errors

  • The rule never applies. alwaysApply is false, the globs do not match the open file, and the description is too vague; fix whichever fits the mode you want.
  • The rules are ignored. They sit in a file that is not .mdc under .cursor/rules/, or in an old root-level .cursorrules; move them into .mdc files.
  • Two rules contradict each other. The agent may follow either; resolve the conflict before committing.
  • Rules go stale. Review them with the code whenever the framework, test runner, or style guide changes.

FAQ

Where do Cursor rules go?

Store them as .mdc files in .cursor/rules/, version-controlled with the code. Plain .md files there are ignored, except AGENTS.md.

Should I still use a .cursorrules file?

No; move rules from an old root-level .cursorrules into .mdc files under .cursor/rules/, where Cursor reads them.

When should a Cursor rule use alwaysApply: true?

Only for rules needed in every conversation, such as architecture or voice, because each one spends context on every request.

How long should a Cursor rule be?

Cursor’s guidance is under 500 lines per rule; split larger ones into focused files such as typescript.mdc and testing.mdc.

Why does my Cursor rule never apply?

alwaysApply is false, the globs do not match the open file, and the description is too vague; fix whichever fits the activation mode you want.