Cursor Rules for Monorepos
"cursor rules monorepo" is a long-tail query because monorepos expose every weak assumption in AI rules. A single app can survive one broad .cursor/rules/project.mdc file. A monorepo cannot. It has multiple apps, packages, frameworks, test commands, generated clients, ownership boundaries, and local exceptions. If Cursor sees one flat rule file for all of that, the agent will either ignore half the rules or apply the wrong half to the wrong package.
Cursor's .cursor/rules system is useful in monorepos precisely because it is modular. Rules can live at the root. They can live in nested .cursor/rules directories. .mdc frontmatter can use globs to attach rules to file patterns. The trick is to use those semantics as a hierarchy, not as a dumping ground.
The goal is not "more rules." The goal is "the right rule attaches when the right files are in play." That is a different design problem.
Root rules are a map
The root rule should describe the monorepo, not every package. It should be always applied, short, and focused on cross-package constraints.
Example:
---
description: Monorepo map and shared rules
globs:
alwaysApply: true
---
This is a TypeScript monorepo.
- apps/web: customer-facing Next.js app.
- apps/admin: internal admin app.
- packages/ui: shared UI primitives.
- packages/api-client: generated API client.
- packages/config: shared lint, TypeScript, and test config.
Shared rules:
- Use npm only. package-lock.json is canonical.
- Do not import from one app into another app.
- Do not edit packages/api-client by hand; update the schema and regenerate.
- Shared UI moves to packages/ui only after two apps need it.
- Project-wide policy lives in AGENTS.md.
That root file gives Cursor enough orientation to avoid obvious mistakes. It does not explain every Next.js route convention, admin table pattern, or generated client edge case. Those belong in scoped rules.
If the root rule has to scroll for pages, it is doing the wrong job. A root monorepo rule is an index and a boundary list.
Package rules own local behavior
Each package or app should get rules only when it has real local behavior. Do not create a .mdc file for every directory just to look organized. Create one when the agent needs different commands, architecture, or style in that area.
For apps/web:
---
description: Web app frontend rules
globs: "apps/web/**/*.{ts,tsx,css,mdx}"
alwaysApply: false
---
- App routes live under apps/web/src/routes/.
- Use packages/ui primitives before app-local primitives.
- Marketing copy lives in apps/web/content/.
- Run npm run test -- --filter web for web-only changes when available.
- Above-the-fold media must set width/height or aspect-ratio.
For apps/admin:
---
description: Admin app implementation rules
globs: "apps/admin/**/*.{ts,tsx}"
alwaysApply: false
---
- Admin tables use the local DataGrid wrapper.
- Do not import customer-facing components from apps/web.
- Internal-only copy does not need marketing review.
- Permission checks must use apps/admin/src/auth/guards.ts.
For packages/ui:
---
description: Shared UI package rules
globs: "packages/ui/**/*.{ts,tsx,css}"
alwaysApply: false
---
- Components exported from packages/ui must be app-agnostic.
- No imports from apps/*.
- New public components need a Storybook story and one usage example.
- Keep visual tokens in packages/ui/src/tokens.css.
These rules are narrower than the root rule. Cursor can attach them when files from that area are referenced. The agent sees local constraints without paying the context cost for every package in every task.
Globs need to match the way people work
The globs field is where many monorepo rules fail. Teams write globs that match the package, but not the files the agent actually references. Or they write globs so broad that every local rule attaches constantly.
Bad:
globs: "**/*"
That is just alwaysApply with extra confusion.
Usually bad:
globs: "*.tsx"
In a monorepo, this may only match files relative to an unexpected location or be too broad conceptually. Be explicit:
globs: "apps/web/**/*.{ts,tsx}"
For generated packages:
globs: "packages/api-client/**/*"
For config packages:
globs: "packages/config/**/*.{js,cjs,mjs,json,ts}"
Globs should reflect ownership boundaries. If one rule applies to both apps/web and packages/ui, either it is a root shared rule or it should be split into two rules with different examples.
Nested rules can reduce noise
Cursor supports .cursor/rules directories in subdirectories. In a monorepo, that lets you keep local rules near the package:
project/
.cursor/rules/
monorepo.mdc
apps/
web/
.cursor/rules/
web.mdc
admin/
.cursor/rules/
admin.mdc
packages/
ui/
.cursor/rules/
ui.mdc
Nested rules are useful when package owners maintain their own rules. They make ownership visible. A frontend team can review apps/web/.cursor/rules/web.mdc without scanning backend policy. A platform team can own packages/config/.cursor/rules/config.mdc.
The risk is fragmentation. Nested rules should narrow root rules, not contradict them. If root says "use npm only" and a nested rule says "use pnpm in this app," that needs an explicit reason, not silent conflict.
Turborepo-style setup
A Turborepo-style repo often has task pipelines and package filters. The root rule should name the package manager and common task shape:
---
description: Turborepo command rules
globs:
alwaysApply: true
---
- Use npm only.
- Root build: npm run build.
- Package-scoped tasks use npm run <task> -- --filter <package> when the script supports it.
- Do not add package-level lockfiles.
- Do not edit generated .turbo/ or dist/ output by hand.
Then package rules can name package-local validation:
---
description: packages/ui validation rules
globs: "packages/ui/**/*.{ts,tsx,css}"
alwaysApply: false
---
- Run npm run test -- --filter @acme/ui for UI package behavior changes.
- Run npm run storybook:build when public component examples change.
The key is to avoid teaching the whole pipeline in every package rule. Root owns shared command grammar. Package rules own local expectations.
Nx-style setup
Nx-style repos often have project names and affected commands. Root rules can tell the agent how to stay scoped:
---
description: Nx monorepo rules
globs:
alwaysApply: true
---
- Use Nx project names from project.json; do not guess package names.
- Prefer affected commands for broad changes.
- Project-specific commands should target the project touched.
- Do not move files across projects without updating project.json and imports.
A local app rule can then say:
---
description: Admin Nx project rules
globs: "apps/admin/**/*.{ts,tsx}"
alwaysApply: false
---
- Nx project name: admin.
- Run npm run nx -- test admin for admin-only logic changes.
- Admin auth boundaries live in apps/admin/src/auth/.
This prevents the agent from applying generic monorepo advice where Nx metadata is the real source of truth.
Per-package vs root: the decision test
Put a rule at root if it applies across the repo and should be visible in every task:
- Package manager.
- Branch policy.
- Generated-file policy.
- Cross-app import bans.
- Secret-handling rules.
- Canonical source-of-truth pointers.
Put a rule in a package if it only applies when touching that package:
- Local component conventions.
- Package-specific test commands.
- Local auth boundaries.
- Framework-specific file layout.
- Generated client regeneration steps.
If a rule mentions two packages, pause. It may be a root boundary rule, or it may be a workflow procedure that belongs in docs. Do not put cross-package process in one package's local Cursor rule and hope the other package owner sees it.
Hierarchy resolution needs documentation
Even if Cursor handles nested rules, humans need to understand the intended hierarchy. Add a short root note:
Rule hierarchy:
- AGENTS.md owns cross-agent project policy.
- Root .cursor/rules owns Cursor-wide monorepo orientation.
- Nested .cursor/rules files may narrow behavior for their package.
- Nested rules must not contradict AGENTS.md unless the override is explicit.
That note is small, but it prevents review confusion. It tells contributors where to add a new rule and where not to.
Keep rules testable
A Cursor rule that says "follow best practices for monorepos" is useless. A rule that says "do not import from apps/web into apps/admin" can be tested with eslint. A rule that says "packages/api-client is generated" can be backed by CI. A rule that says "use package filters for package-scoped tests" can be checked in review.
The best monorepo Cursor rules have verbs that change behavior:
- Do not import.
- Run this command.
- Update this schema.
- Keep this generated directory untouched.
- Use this wrapper.
They avoid vibes:
- Be consistent.
- Keep things clean.
- Use good architecture.
- Follow monorepo best practices.
Monorepos already have enough implicit complexity. Rules should remove ambiguity, not add prose.
The maintenance loop
Review .cursor/rules whenever package boundaries change. A new app, renamed package, moved generated client, or changed test command should trigger a rule update. The same change should usually touch AGENTS.md if it affects cross-agent policy.
AgentLint is useful here because monorepos make drift easy. It can check AGENTS.md, .cursor/rules, CLAUDE.md, CI, hooks, package files, and referenced commands together, so one package rule does not quietly contradict the root harness.