---
title: "How to write a Cursor rule"
slug: "write-a-cursor-rule"
category: "howto"
tags: ["howto", "cursor", "ai-agents", "typescript", "tutorial", "tooling", "workflow", "coding"]
status: "stable"
last_updated: 2026-08-14
summary: "Write .cursor/rules/*.mdc files with voice rules, scope conventions, code style, and test expectations so Cursor's Agent mode follows project standards."
related: ["[[tooling/claude-code-workflow]]", "[[ai-agents/claude-code]]", "[[prompt-engineering/prompt-design]]", "[[coding/typescript]]", "[[coding/typescript-strict-mode]]", "[[ai-agents/system-prompts]]", "[[howto/set-up-claude-code]]"]
---

## Overview

A Cursor project rule is a persistent system prompt that Cursor injects into AI conversations in the project. A well-written rules file produces code that fits the codebase from the first suggestion, without per-session re-prompting. The current format is one or more `.mdc` files under `.cursor/rules/`, not the legacy single `.cursorrules` file at the repo root: Cursor's Agent mode, the default mode since 2026, silently ignores `.cursorrules`, so a team relying on it stops getting rule-following behavior without any error. The principles that make a system prompt effective are the same ones in [[ai-agents/system-prompts]]; this guide applies them to a TypeScript project.

## Prerequisites

- Cursor IDE installed, recent enough that Agent mode is the default (any 2026 release).
- A TypeScript project with a `tsconfig.json`. The rules reference TypeScript-specific conventions.
- Familiarity with the project's style, testing framework, and folder structure. Rules you cannot verify are rules that drift.

## Steps

### 1. Create the rules directory

```bash
mkdir -p .cursor/rules
touch .cursor/rules/project-conventions.mdc
```

Cursor reads every `.mdc` file under `.cursor/rules/` automatically. Split rules into multiple files by concern (`typescript.mdc`, `testing.mdc`) once the project grows past one topic; a single file is fine to start. Commit the directory with the code; it is project documentation.

### 2. Write the frontmatter

Each `.mdc` file opens with YAML frontmatter that controls when Cursor loads it: `description` (used for intelligent matching when `alwaysApply` is false), `globs` (file patterns that force-activate the rule), and `alwaysApply` (loads on every conversation regardless of context).

```yaml
---
description: Project conventions for the Next.js storefront
globs: ["**/*.ts", "**/*.tsx"]
alwaysApply: false
---
```

Set `alwaysApply: true` only for rules that must load every time, such as voice or architecture rules; overusing it burns context budget on every conversation.

### 3. Open with a mission statement

Start the markdown body with one or two sentences that name the project, its purpose, and the main stack.

```
This is a Next.js 15 + TypeScript e-commerce storefront.
The codebase uses the App Router, Tailwind CSS, Prisma ORM, and Postgres.
```

Keep it specific. "This is a web project" adds no signal. See [[prompt-engineering/prompt-design]] for the general rule on specificity.

### 4. Set voice and style rules

```
Voice rules:
- Lead with the implementation, then the explanation.
- Prefer short, named functions over long anonymous callbacks.
- No commented-out code in commits.
- No TODO comments without a ticket reference.
```

Match the voice rules to what the team actually enforces in code review. If the team does not care about TODO comments, omit that rule.

### 5. Define TypeScript conventions

```
TypeScript rules:
- Strict mode is on. Never use `any`; use `unknown` and narrow it.
- Prefer `interface` for public API shapes; use `type` for unions and mapped types.
- Export types from `types.ts` in each feature folder.
- Use `zod` for runtime validation at API boundaries.
- Avoid `as` casts; if a cast is unavoidable, add a comment explaining why.
```

These map directly to the patterns in [[coding/typescript-strict-mode]]. See [[coding/typescript]] for the full type-level conventions.

### 6. Set folder and file conventions

```
File conventions:
- React components live in `src/components/<ComponentName>/index.tsx`.
- Server actions live in `src/app/actions/<feature>.ts`.
- Database queries live in `src/lib/db/<entity>.ts`.
- One component per file. Do not export multiple components from one file.
- Barrel files (`index.ts`) are allowed only in `src/components`.
```

Concrete paths prevent the AI from placing files in unexpected locations.

### 7. Define test expectations

```
Testing rules:
- Tests live next to the file they test: `foo.ts` and `foo.test.ts`.
- Use Vitest, not Jest.
- Test behavior, not implementation. Do not test private methods.
- Every function exported from `src/lib` must have at least one test.
- Use `describe` blocks to group related tests; keep `it` descriptions in plain English.
```

See [[coding/testing]] for the broader testing philosophy.

### 8. Add a complete example

Examples anchor the rules in concrete output. Include at minimum one before/after pair.

```
Example: API route with validation

// Good
import { z } from "zod"
const schema = z.object({ email: z.string().email() })
export async function POST(req: Request) {
  const body = schema.safeParse(await req.json())
  if (!body.success) return Response.json({ error: body.error.flatten() }, { status: 400 })
  // ...
}

// Avoid: no runtime validation
export async function POST(req: Request) {
  const { email } = await req.json()
  // email is any; no validation
}
```

### 9. Scope and length

Keep each `.mdc` file under 200 lines. Rules past that point compete for attention and dilute signal. Prioritize the conventions that are:

1. Hard to enforce via a linter.
2. Frequently violated by AI-generated code.
3. Specific to this project (not just TypeScript best practices generally).

## Verify it worked

1. Open a new Cursor chat in Agent mode (not a file-level inline suggestion) and ask: "How should I structure a new API route in this project?"
2. Confirm the answer references the folder conventions and Zod validation from the rules.
3. Ask it to write a test for a utility function. Confirm it uses Vitest and places the test file correctly.
4. If the AI ignores the rules, confirm the files are `.mdc` files under `.cursor/rules/`, not a legacy `.cursorrules` file; Agent mode does not read `.cursorrules` at all.

## Common errors

- The AI ignores the rules and no error appears. The project still relies on a root `.cursorrules` file; Cursor's Agent mode, the default since 2026, silently ignores it. This is the failure most likely to bite a team that set rules up a year or two ago and never migrated. Move the content into `.cursor/rules/*.mdc`.
- A rule never activates. `alwaysApply` is `false` and the `globs` pattern does not match the file being edited, and the `description` is too vague for intelligent matching to select it. Broaden the globs or sharpen the description.
- Rules conflict with each other. "Use functional components" and "class components are fine for stateful logic" cannot coexist. Resolve the conflict before committing.
- Rules are too abstract. "Write clean code" is not a rule; "functions must be under 30 lines" is. Replace abstractions with measurable criteria.
- Rules become stale as the stack evolves. Treat the rules directory like a `CLAUDE.md`: review it when the framework, test runner, or style guide changes. See [[ai-agents/claude-code]] for the anchor-file maintenance pattern.

## Related

- [[tooling/claude-code-workflow]]
- [[ai-agents/claude-code]]
- [[prompt-engineering/prompt-design]]
- [[coding/typescript]]
- [[coding/typescript-strict-mode]]
- [[ai-agents/system-prompts]]
- [[howto/set-up-claude-code]]
