Custom rules and templates in Cursor
PLA Cursor rules library is a set of .mdc files in .cursor/rules/ that gives Cursor’s Agent a team’s conventions and step-by-step templates, each meant to load only when its frontmatter says so. The library pays off only when it is small, scoped, and checked in CI, because a rule that never loads or contradicts the code fails silently.
This page is for the developer or tech lead whose repository already has a few project rules and now wants them to hold up for a whole team. Six months in, the pattern is familiar: 20 rule files, two of them with no frontmatter, so nobody can say when they load, an “API conventions” rule that still points at a file deleted in March, and Agent output that varies by who prompted it. If you are writing your first rule, start with project rules in Cursor and come back.
What a tested rules library gives you
Section titled “What a tested rules library gives you”- A decision table for what goes in an always-on rule, a file-scoped rule, a template, a skill, or a linter.
- Five rule templates you can copy and adapt: core, file-scoped, workflow, manual procedure, and review checklist.
- Three copy-paste prompts that mine your review history for rules, turn canonical files into a template, and audit the library.
- A CI script that fails when a rule has no frontmatter, a glob matches nothing, or an
@reference is dead. - A probe routine that shows the rules change Agent’s output, checked by commands rather than by reading diffs.
Cursor’s UI labels and rule options change often; confirm them against cursor.com/docs/rules before you train a team.
Which frontmatter decides when a rule loads?
Section titled “Which frontmatter decides when a rule loads?”A rule’s frontmatter carries three keys: alwaysApply, description, and globs. Cursor’s own plugin scaffold, the create-plugin skill in cursor/plugins, writes rules as .mdc files with description, alwaysApply and an optional globs (checked 2026-10-02), and on 2026-10-12 Cursor’s rules docs confirm the same three frontmatter keys. The table maps the four combinations to the load behaviour Cursor has described for them. That behaviour and the rule-picker labels remain as reported before 2026-10-02 and were not re-checked against cursor.com/docs/rules on 2026-10-12, so read the first and last columns as what to expect, not as a guarantee, and confirm each mode in your version with the probes below.
| Expected load behaviour | alwaysApply | description | globs | Picker label (reported, unconfirmed) |
|---|---|---|---|---|
| Every Agent session | true | optional | not needed | Always Apply |
| When a file matching the pattern is in context | false | optional | set | Apply to Specific Files |
| When Agent reads the description and judges it relevant | false | set | — | Apply Intelligently |
Only when someone @-mentions the rule in chat | false | — | — | Apply Manually |
Five points shape every decision below:
- Use
.mdcwith frontmatter. “Project rules live in.cursor/rulesas.mdcfiles”, and “A plain.mdfile in.cursor/rulesis ignored by the rules system” (Cursor docs, checked 2026-10-12). It is also the format Cursor’s own plugin scaffold writes, withdescription,globsandalwaysApplyin the frontmatter. A rule without frontmatter gives Cursor nothing to scope it by. AGENTS.mdandCLAUDE.mdload beside.cursor/rules/. “Cursor supports AGENTS.md in the project root and subdirectories”, and nested files “are combined with parent directories, with more specific instructions taking precedence.” “Cursor readsCLAUDE.mdfiles the same way it readsAGENTS.md”, and they “are always applied to every conversation, regardless of anyalwaysApplyfrontmatter setting”, so count aCLAUDE.mdin the always-on budget below. Cursor’s help calls a.cursorrulesfile in the project root “legacy and will be deprecated” and says to migrate it to a project rule set to Always Apply (checked 2026-10-12).- Write several globs on one
globsline, separated by commas:src/**/*.tsx, src/**/*.css. The CI script below accepts that comma list, a YAML list under an emptyglobs:, and brace patterns such assrc/**/*.{ts,tsx}. - Rules are written for Agent. Cursor’s rules documentation calls them “system-level instructions to Agent”, and Cursor’s help says: “Rules only apply to Agent (Chat). They do not apply to Tab completion, Inline Edit, or Bugbot PR reviews.” (checked 2026-10-12). Pull request review belongs to Bugbot, which takes its own guidance (see Bugbot).
- Several sources can apply at once. Team Rules, project rules and User Rules can all reach one session. When they conflict, Cursor’s rules help page says “When rules conflict, precedence is: Team Rules > Project Rules > User Rules” (read 2026-10-13). Confirm the order in your Cursor version with a probe that gives two rules conflicting instructions.
What belongs in a rule, and what does not?
Section titled “What belongs in a rule, and what does not?”Most weak libraries come from putting the right instruction in the wrong place. Ask what enforces the instruction best, then choose the cheapest mechanism that does it.
| The instruction is… | Put it in | Why |
|---|---|---|
| Needed on almost every task: stack, commands, boundaries | Always-on rule (alwaysApply: true), under ~40 lines | Every session pays for it in context |
| True only in one area: API routes, migrations, UI components | File-scoped rule (globs) | Meant to load only where it matters |
| A multi-step procedure: “add an endpoint”, “add a feature flag” | Description-matched rule that points at canonical files | The description decides; the body stays short |
| Rare but risky: a data backfill, a release | Manual rule (no description, no globs) | Meant to load only when someone asks for it |
| A long, repeatable workflow with scripts | A skill | Rules suit short guidelines; skills carry multi-step procedures and their scripts |
Mechanically checkable: import order, any, naming | A linter or type rule | The check runs on every commit, not only when Agent remembers |
| Something Agent already does by default | Nowhere | It costs context and teaches nothing |
The last two rows matter most. Leave style to the linter, and add a rule only after Agent repeats the same mistake. Pruning context files explains why the evidence now favours fewer, sharper instructions.
Short, pointer-style procedures can stay as description-matched rules. Once one needs scripts or grows past ~40 lines, convert it to a skill. Cursor lists /migrate-to-skills among its built-in skills for this (checked 2026-10-12); type / in Agent chat to see it in your version.
Build the library in five templates
Section titled “Build the library in five templates”Start from evidence, not from a style guide. List the corrections your reviewers repeat; the first prompt after the templates does this from your merged pull requests’ review comments, so every rule answers a real mistake. Then write the templates in this order.
-
Write one short core rule. Save it as
.cursor/rules/core.mdc. It carries only what every task needs: the commands that prove work is done and the boundaries Agent must not cross.---description: Commands, done criteria and hard boundaries for every taskalwaysApply: true---Stack: TypeScript (strict), Next.js App Router, Drizzle on PostgreSQL, Vitest.Done means all of these pass:- pnpm lint- pnpm typecheck- pnpm testBoundaries:- Never edit files under src/db/migrations/ that are already merged; add a new migration.- Never add a dependency without saying why in the summary.- Log with the logger in @src/lib/logger.ts, never console.log.Before you report a task as done, follow @.cursor/rules/self-review.mdc. -
Scope area conventions to their files. One rule per area, named for the area (here
.cursor/rules/api-routes.mdc), pointing at the canonical example instead of copying it.---globs: src/app/api/**/*.tsalwaysApply: false---API route conventions:- Validate the request body with a Zod schema from @src/lib/validators/index.ts before any other work.- Throw AppError from @src/lib/errors.ts; the route wrapper turns it into the JSON error shape.- Every query filters by organizationId. A route without it is a security bug.Canonical route: @src/app/api/users/route.ts -
Turn repeated procedures into workflow templates. Save this one as
.cursor/rules/new-endpoint.mdc. The description is what Agent is meant to match against, so write it the way someone would ask for the task.---description: Adding a new API endpoint or resource end to end (schema, route, service, tests)alwaysApply: false---When adding an endpoint:1. Schema: add the Drizzle table or column and a new migration in src/db/migrations/.2. Validation: add the Zod schema next to the others in src/lib/validators/.3. Service: business logic in src/lib/services/, no HTTP types in it.4. Route: follow @src/app/api/users/route.ts exactly; copy its structure, not its logic.5. Tests: one success path and one AppError path per handler, next to the route.Finish by running the commands under "Done means" in the core rule and reporting their output. -
Keep risky procedures manual. Save the backfill checklist as
.cursor/rules/data-backfill.mdcso it is meant to load only when someone types@data-backfill.---alwaysApply: false---Data backfill procedure:- Write the backfill as a script in scripts/backfills/ with a --dry-run flag that prints row counts.- Batch writes in chunks of 1,000 and make the script safe to re-run.- Never change a column type in place: add a new column, backfill, switch reads, then drop.- Stop after the dry run and show its output; a human runs the real backfill. -
Close every task with a self-review. Save the checklist Agent applies to its own diff as
.cursor/rules/self-review.mdc. Its description is there so Agent can pick it up at hand-back, and the last line of the core rule names it as well. Do not assume that@line loads the file: the self-review probe below shows whether the checklist ran, because only this rule asks for theCHANGED FILES:block. Review of the pull request itself is Bugbot’s job; see Bugbot.---description: Final self-review before reporting a task as donealwaysApply: false---Before you say the task is done:- Run the commands under "Done means" in the core rule and paste the last lines of each.- Write a line that says exactly CHANGED FILES: and under it every file you created, changed or deleted, one repository-relative path per line with no bullets, then one blank line. Then give one line per file on why.- Name any test you skipped, weakened or deleted. The answer should be none.- Name every new dependency, environment variable or migration.
Commit every file under .cursor/rules/ so the team reviews rule changes like code.
How do you prove the rules work without reading every diff?
Section titled “How do you prove the rules work without reading every diff?”A rule is an untested instruction until something checks it. Use two layers: a static check that the library is loadable, and behaviour probes that show Agent follows it.
Fail CI when the library drifts
Section titled “Fail CI when the library drifts”Save this as scripts/check-cursor-rules.mjs and run node scripts/check-cursor-rules.mjs in the same CI job as your linter. It needs Node 22 or later for fs.globSync.
// Fails CI when the .cursor/rules library drifts. Node 22 or later.import { existsSync, globSync, readFileSync } from 'node:fs';import { dirname, join } from 'node:path';
const MAX_LINES = 200; // keep each rule short; tune to your libraryconst ALWAYS_BUDGET = 120; // lines loaded into every Agent sessionconst errors = [];const warnings = [];let alwaysLines = 0;
// Accepts `globs: a, b`, `globs: ["a", "b"]`, brace globs such as// `src/**/*.{ts,tsx}`, an empty `globs:`, and a YAML list under `globs:`.function readGlobs(fm) { const line = fm.match(/^globs:[ \t]*(.*)$/m); if (!line) return []; const value = line[1].trim(); let items; if (value) { items = value.replace(/^\[|\]$/g, '').split(/,(?![^{]*\})/); } else { items = []; for (const next of fm.slice(line.index + line[0].length).split(/\r?\n/).slice(1)) { const item = next.match(/^\s*-\s*(.+)$/); if (!item) break; items.push(item[1]); } } return items.map((g) => g.trim().replace(/^["']|["']$/g, '')).filter(Boolean);}
for (const file of globSync('.cursor/rules/**/*.{md,mdc}')) { const text = readFileSync(file, 'utf8'); const fm = text.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1]; if (fm === undefined) { if (file.endsWith('.md')) { warnings.push(`${file}: no frontmatter, so it cannot be scoped; prefer .mdc with description, globs or alwaysApply`); } else { errors.push(`${file}: no frontmatter`); } continue; } const lines = text.split('\n').length; if (lines > MAX_LINES) errors.push(`${file}: ${lines} lines (max ${MAX_LINES})`); if (/^alwaysApply:\s*true\b/m.test(fm)) alwaysLines += lines; for (const g of readGlobs(fm)) { if (globSync(g, { exclude: (p) => p.includes('node_modules') }).length === 0) { errors.push(`${file}: glob "${g}" matches no file`); } } for (const [, ref] of text.matchAll(/(?:^|\s)@([\w./-]+\.\w+)/gm)) { if (!existsSync(ref) && !existsSync(join(dirname(file), ref))) { errors.push(`${file}: @${ref} does not exist`); } }}if (alwaysLines > ALWAYS_BUDGET) { errors.push(`always-apply rules total ${alwaysLines} lines (budget ${ALWAYS_BUDGET})`);}for (const w of warnings) console.warn(`warning: ${w}`);console.log(errors.length ? errors.join('\n') : 'cursor rules: OK');process.exit(errors.length ? 1 : 0);Both limits are starting points: the 200-line ceiling per rule and the 120-line always-on budget should be tuned against your own token use. Add .cursor/rules/ to CODEOWNERS so one named owner signs off on every rule change.
Probe behaviour with checks, not by reading
Section titled “Probe behaviour with checks, not by reading”Keep a short docs/rule-probes.md in the repository. Each row is a task you give Agent in a fresh chat and a command whose result decides whether the rules worked.
| Probe task | Rule it exercises | Deterministic check |
|---|---|---|
| “Add GET /api/invoices/[id]” | API routes and endpoint workflow | pnpm test src/app/api/invoices passes and rg -c organizationId src/app/api/invoices is at least 1 |
| “Log the retry count in the payment service” | Core rule | rg -n "console\.log" src/lib/services/payment* finds nothing |
| “@data-backfill: fill invoices.currency” | Manual procedure | rg -l -- --dry-run scripts/backfills/ returns the new script, and git diff --stat shows no migration or data change was applied |
| Any of the tasks above | Self-review rule loaded | The command below exits 0 |
For the self-review row, save Agent’s final message as /tmp/summary.txt. The command compares the CHANGED FILES: block with what git sees, including new and deleted files. A missing block fails it too, which means the rule never loaded.
diff <(sed -n '/^CHANGED FILES:$/,/^$/p' /tmp/summary.txt | sed '1d;/^$/d' | sort -u) \ <(git ls-files --modified --deleted --others --exclude-standard | sort -u) \ && echo "self-review: file list matches"The checks are commands, but someone still has to start each task. To run the probes unattended, script them with the Cursor SDK: the @cursor/sdk package calls Cursor’s agent from your own code (Node 22.13 or later), so a scheduled job can give Agent each probe task in a fresh run on a clean checkout and then execute the check column. The SDK lets you choose which settings layers apply (project, user, team, plugins), so make sure the run loads the project layer that holds .cursor/rules/. Run the probe job in a disposable sandbox or a cloud agent, with no production secrets in its environment and a read-only repository token.
Run the probes after any rule change and before you promote a rule to Team Rules. A failed probe means the rule is unclear, never loaded, or contradicted by another rule; the next section tells you which. For the same idea across Claude Code, Codex and Cursor, see proving every agent loaded the shared rules.
How do you share rules across repositories and the team?
Section titled “How do you share rules across repositories and the team?”Pick by who must follow the rule and whether they may opt out.
- One repository: project rules in
.cursor/rules/, committed and owned throughCODEOWNERS. - Every repository, set by admins: Team Rules, managed from the Cursor dashboard. They apply across the team’s repositories and win over a project rule when the two conflict (Cursor’s rules help page, read 2026-10-13), so promote a rule only after it passes its probes in one repository. Since Cursor 3.21, a team rule with file patterns applies only to the files that match them (checked 2026-10-12).
- A reusable bundle: to reuse a set of rules across repositories, package them in a plugin with a
rules/folder of.mdcfiles. Cursor’s docs describe plugins as packaging “rules, skills, agents, commands, MCP servers, and hooks into distributable bundles”. Cursor’s own cursor/plugins repository ships rules this way (every rule hasdescriptionandalwaysApply), and its create-plugin scaffold listsglobsas optional. - Several agents on one repository: keep one source and generate Cursor’s files from it; see one rules source for every agent.
Team-wide rollout, ownership and review norms are covered in team collaboration in Cursor.
What breaks in a Cursor rules library?
Section titled “What breaks in a Cursor rules library?”A rule never loads, or loads everywhere. The frontmatter is missing or broken: no --- fences, a misspelled field, or a plain .md file, which Cursor’s rules system ignores (checked 2026-10-12). Recovery: save it as .mdc with description, globs or alwaysApply, then run the CI script; it flags a rule with no frontmatter.
A description-matched rule is skipped for the task it was written for. Its description names a topic (“API stuff”) rather than a task. Recovery: rewrite the description as the request a developer would type, then rerun the probe. If it still misses, scope it with globs instead.
A glob rule never fires. The pattern points at a path that moved, or it is broader or narrower than you think. Recovery: prefer explicit patterns such as src/**/*.ts, and let the CI script confirm that each glob matches files.
Agent follows a rule that contradicts the code. The rule describes last year’s pattern while the canonical file changed. Recovery: replace pasted code with @ references so the rule follows the file, and delete rules the audit prompt marks as contradicted.
Context fills up before the task starts. Always-on rules grew one line at a time. Recovery: move area-specific lines to glob rules, move procedures to skills, and hold the always-on budget in CI.
A Team Rule overrides a project rule. When rules conflict, Team Rules win (Cursor’s rules help page, read 2026-10-13), so a generic organization rule can beat a precise repository rule. Recovery: run the conflicting-instructions probe to confirm the order in your version, keep Team Rules to policy (security, data handling), and leave code conventions to project rules.
The team expects rules in Tab or in pull request review. Cursor’s help says: “Rules only apply to Agent (Chat). They do not apply to Tab completion, Inline Edit, or Bugbot PR reviews.” (checked 2026-10-12). Bugbot takes its own guidance. Recovery: give Bugbot its own review guidance (see Bugbot) and enforce style with the linter.
Where to go next with Cursor rules
Section titled “Where to go next with Cursor rules”- Project rules in Cursor: the prerequisite, for your first rule files.
- Team collaboration in Cursor: rolling a rules library out to a team.
- Token management: what always-on rules cost in context.
- Pruning context files: deleting instructions that no longer earn their place.
- Plugins and marketplaces: distributing rules with skills, hooks and MCP servers.