Where the single root file breaks
A single instruction file is the right answer for a single application, and it stops being the right answer at roughly the moment a second package appears with different rules.
The failure is not dramatic. Someone working in packages/ui gets a paragraph about database migrations, a paragraph about the Python data jobs, and the React conventions they actually needed, all at equal volume. Nothing is wrong, and everything is slightly diluted. Then the file grows, because each team adds its own section, and the dilution becomes the dominant effect.
A monorepo instruction file that tries to be complete is competing with itself. The fix is not better writing, it is scope.
Mechanism one: nearest file wins
Put an instruction file in each package and let the tool pick the one closest to the work. This is how the file-based formats behave:
AGENTS.md # the repo: how to build, how to test, the rules
packages/
ui/
AGENTS.md # components, storybook, no data fetching here
api/
AGENTS.md # handlers, zod, migrations
jobs/
AGENTS.md # python, uv, the one cron that mattersClaude Code reads the file in the working directory and every directory above it, so a session started in packages/api gets the root file and the package file both. It also picks up a subdirectory's file when it reads a file from that subdirectory, which is the behaviour that makes per-package files worth writing at all.
AGENTS.md works the same way across the tools that read it, nearest file first, and the format's own documentation points at monorepos as the case it was designed for. Which of the two Claude Code actually reads is a separate question with a trap in it, worth knowing before you commit a tree of them.
Mechanism two: one file, scoped by globs
The other approach keeps the rules in one directory and attaches a path pattern to each one. Cursor and Copilot both do it this way, with different spellings of the same idea:
| Tool | Where the scope is written |
|---|---|
| Cursor | globs: in the frontmatter of a .mdc file under .cursor/rules/ |
| Copilot | applyTo: in the frontmatter of a .instructions.md file under .github/instructions/ |
| Claude Code | Path-scoped files under .claude/rules/, which keep loading alongside an AGENTS.md |
---
description: "Route handlers"
globs: packages/api/**/*.ts
alwaysApply: false
---
Handlers validate with zod before touching the database. Errors return the
shared `ApiError` envelope.The advantage is that every rule is visible in one directory, which matters when nobody can remember how many instruction files the repository has. The cost is that a glob is a claim about your directory layout, and a rename makes it silently false. The frontmatter truth table covers which combinations of glob and flag actually load.
Cursor also allows a .cursor/rules/ directory inside a package, so the two mechanisms are not exclusive. Most repositories end up using both without deciding to.
Which one to pick
The deciding question is who edits the rules. If each package has a team that owns it, use per-package files: the rules live next to the code they describe, they move when the package moves, and a pull request that changes a convention changes it in the same diff.
If the layout churns, or one or two people set conventions for everyone, use globs: a rename is one line to fix in one place, and nobody has to notice that a new package needs its own file.
The mistake either way is the same, which is writing both and then keeping neither current.
What stays at the root
The root file answers the questions that have one answer for the whole repository, and refuses the rest:
# Monorepo
pnpm workspaces. Run everything from the root: `pnpm -F @acme/api test`,
never `cd packages/api && npm test`.
- `pnpm build` builds in dependency order. If it fails in a package you did
not touch, the fix is usually a stale build, so `pnpm clean` first.
- Cross-package imports go through the package entry point, never a deep
path into `src/`.
- A change touching two packages is one pull request, not two.
Each package has its own AGENTS.md with the rules for that package. Read the
one next to the code you are changing.That last line does real work. It tells a reader, human or not, that this file is deliberately incomplete, which stops the next person from solving the incompleteness by pasting their team's conventions into it.
Everything else follows the usual budget. A root file in a monorepo is loaded by every session in every package, so it is the most expensive text in the repository, and the case for keeping it short is proportionally stronger. The worked examples apply unchanged.
The duplication that rots
Six package files that each restate the test command is six places to be wrong, and they will not go wrong together. One of them will keep saying yarn test for a year after the migration.
So the rule is that a package file may only contain what is untrue of the other packages. If a line would be correct pasted into a sibling, it belongs at the root and nowhere else. The question to ask of every line in a package file is whether it is a difference, and if the answer is no, the line is duplication waiting to drift.
When something genuinely is shared but only relevant to a few packages, the import is better than the copy. Claude Code resolves @path references inside a file, so a package file can pull in a shared fragment and the fragment stays single-sourced:
@../../docs/typescript-conventions.md
## This package
Handlers live in `src/routes/`, one file per resource. Anything shared goes in
`src/lib/`, and if it is shared with another package it does not go here at all.Check it from inside the package
The last step is the one people skip. Open a session in packages/api, not at the root, and ask the tool what it loaded. The answer is frequently not what the directory layout implies, and every tool in this cluster has a way of telling you.
The two things worth confirming: that the package file is present, and that the other packages' files are absent. A tree of instruction files that all load at once is worse than the single root file it replaced, because it is the same volume of text with more places to edit.
If it turns out the right file loaded and the rule still did not happen, the problem has moved from layout to wording, and the diagnosis continues here.