Tinker AI
Read reviews
intermediate 5 min read

Cursor rules for projects with multiple stacks: per-directory .cursorrules

Published 2026-04-27 by Owner

Most repos I work in aren’t pure-language. There’s a TypeScript Next.js frontend, a Python FastAPI backend, some bash for deploys, maybe a Terraform config for infra. A single .cursorrules at the root that tries to cover all four ends up either too generic to help or full of conditional logic the model ignores.

Cursor 0.42+ supports per-directory rule scoping. It’s not advertised heavily and the docs are thin. Here’s the working pattern.

The .cursor/rules/ directory

Create .cursor/rules/ at the repo root:

.cursor/
  rules/
    frontend.mdc
    backend.mdc
    infra.mdc
    docs.mdc

Each .mdc file has frontmatter specifying when it applies:

---
description: Frontend conventions for the Next.js app
globs: ["apps/web/**/*.{ts,tsx}", "packages/ui/**/*.{ts,tsx}"]
alwaysApply: false
---

# Frontend rules

The frontend is Next.js 15 with the App Router. Follow these conventions:

- All pages and components are React Server Components unless they explicitly need client state
- Add 'use client' only when necessary; document the reason in a comment
- Server actions for mutations; tRPC for queries (server-to-server)
- Use Tailwind v4 syntax; no @apply in component files
- Forms use react-hook-form with Zod resolvers
- Tests with Vitest; component tests with @testing-library/react

The globs field is the key. When you ask Cursor to edit a file matching one of these globs, this rule file is included in context. Files outside the globs don’t get this rule, which keeps context focused.

A backend rule file

---
description: Backend conventions for the FastAPI service
globs: ["services/api/**/*.py", "services/worker/**/*.py"]
alwaysApply: false
---

# Backend rules

Python 3.12, FastAPI, SQLAlchemy 2.0, uv for package management.

- All endpoint functions are async
- Use Pydantic v2 models for request/response validation
- Database access goes through repositories in `services/api/repos/`; routes never call SQLAlchemy directly
- Background jobs go through Celery with the Redis broker; do not call long-running tasks from within a request
- Logging uses `structlog` with bound context (request_id, user_id) at request entry
- Tests use pytest with httpx.AsyncClient; database tests use the testcontainers fixture
- Type hints required everywhere; mypy --strict passes on every commit

Now when you ask Cursor to write a new endpoint in services/api/routes/users.py, it loads the backend rules. When you ask for a new component in apps/web/components/, it loads the frontend rules. There’s no conflict because each rule file is scoped.

What alwaysApply: false buys you

The alwaysApply: false setting means the rule loads only when a file matching the globs is in context. For a long Cursor session that touches both frontend and backend, you might load both rule files; for a session that stays in the frontend, only the frontend rules load.

This matters because:

  1. Token cost — loading 4 rule files when one would do is wasteful
  2. Context dilution — irrelevant rules confuse the model on tasks that aren’t about that part of the codebase

Set alwaysApply: true only for project-wide invariants (“never commit secrets”, “always update CHANGELOG.md”).

A meta-rule for the whole project

.cursor/rules/project.mdc:

---
description: Cross-cutting project conventions
alwaysApply: true
---

# Project conventions

This is a [project name] monorepo. Cross-cutting rules:

- All commits use conventional commit format (feat:, fix:, chore:, docs:, refactor:)
- Never commit secrets or API keys; if you find one, stop and tell me
- The deploy is via GitHub Actions on push to main; main is protected
- Do not modify .github/workflows/ without explicit permission
- The CHANGELOG is updated on every PR; user-facing changes go in [Unreleased]

This loads on every interaction regardless of file. Keep it short. Long always-apply rules dilute the per-stack rules.

Rule precedence

When multiple rule files match (e.g., a file matches both frontend.mdc globs and a more specific frontend-tests.mdc glob), Cursor loads both. The model sees both sets of instructions; on conflict, the more specific one tends to win because it’s more recent in the prompt.

I haven’t found cases where this caused problems in practice. The rules tend to compose naturally if each file is scoped tightly.

Migration from a single .cursorrules

If you already have a long .cursorrules at the root, the migration:

  1. Copy .cursorrules to .cursor/rules/legacy.mdc with alwaysApply: true
  2. Run a few sessions and notice which sections of the legacy rules get used in which contexts
  3. Split those sections into scoped files with appropriate globs
  4. Delete sections that turn out to be unused
  5. When legacy.mdc is empty, remove it

Don’t try to split everything at once. Let the actual workflow tell you what scoping makes sense.

What this gets you

After moving from a single 200-line .cursorrules to seven scoped files totaling 280 lines:

  • Token cost per session dropped about 30% (loaded rules averaged 80 lines vs the full 200)
  • Suggestion quality improved on language-specific tasks (the model wasn’t being told frontend rules during backend tasks)
  • Onboarding new engineers got cleaner — they could read frontend.mdc to understand the frontend without wading through backend conventions

The setup takes 30 minutes for an existing project. Worth it for any repo that mixes more than two stacks.