Monorepos: scoping instructions to a subtree
Split the memory file per package and half these tools won't load it. Which instruction files your harness reads depends on where you started the session.
The root file that belongs to nobody
In a single-package repository the memory file has one audience. In a monorepo the same file has to work for every package — the Go service and the React app and the shared library at once.
That's also what Anthropic's monorepo guide says: a single root-level file tends to fall into one of two camps.
It keeps growing until it documents every convention of every subsystem, in which case it contains too much and is hard to parse
Or it stays so high-level that it becomes void
If the first scenario happens, you end up with a file that's like the sum of all packages, and a lot of it is wrong, just because it's not applicable wherever you work — in a package written in Go, "run npm test before committing" is simply false, and in 9 out of 10 directories of your monorepo there's no need to say that migrations should be generated and not edited by hand. Every session pays for all of it.
If the second scenario happens, the file becomes a collection of "git is used" and "be careful" statements.
The common solution is to create a file per package, and it's the right one. It just doesn't do what most people have in mind:
In the four tools we started from the repo root, the package's file wasn't in play at all — Codex didn't have it even after reading a file sitting next to it
There's no error either, because the file is there and the agent works in this very directory. It simply never sees it
Where you launch decides what loads
The reason behind this is that every tool assembles instructions by traversing directories from the root of the repository (or the git root, or the workspace) towards the directory you launch the tool from. As it does this, it includes the files found in the ancestor directories — but usually not the ones below, and where it does, they arrive late.
That's why we created a simple two-package fixture and measured it instead of analysing documentation. We made sure that every directory contains the same content under every filename these tools look for, so no tool is disadvantaged:
mono/
AGENTS.md # ROOT_MARKER: alpha-root (+ CLAUDE.md, GEMINI.md, same content)
packages/
api/
AGENTS.md # API_MARKER: bravo-api (+ CLAUDE.md, same content)
src/index.ts
web/
src/main.tsThen we created a prompt for each tool that asks it to return the values of ROOT_MARKER and API_MARKER as they appear in its loaded instructions, and unknown for the ones it doesn't have. From the root of the repository, Claude Code, Codex, Copilot and Kimi Code all returned alpha-root and unknown. Claude Code was the only one that turned that second answer into bravo-api from the root, and only after it had opened a file in packages/api/. Codex found packages/api/AGENTS.md in its own file search, read src/index.ts next to it, and still returned unknown.
Two questions then, and the answers differ per tool:
| Harness | Files above where you launched | Files below where you launched |
|---|---|---|
| Claude Code | loaded in full at launch, root down to cwd | loaded on demand, when it reads a file in that directory |
| Codex CLI | loaded at launch, git root down to cwd | never — it walks down to cwd and stops |
| GitHub Copilot CLI | repo root, cwd, and everything between | directories in the path of a file it's working on |
| Cursor | loaded | nested AGENTS.md applies to that directory and its children |
| Antigravity CLI | the active directory's own file, plus the global one; no ancestor walk documented | not documented |
| Kimi Code CLI | loaded — the root file was there from inside the package | not documented, and not at launch |
So for most of these, the answer to "how do I scope instructions to a subtree" is that you launch the session in that subtree:
codexfrom the root of the repository — you get the root file and that's it, even if you then say "work on the API"cd packages/api && codex— you get the root file and the package file
Most tools also support a rule with its own glob, which is triggered when you touch a file that matches it, regardless of where you launch the tool from. Which one your harness has, and what it's called, is the block below.
What goes up and what goes down
The split that survives contact with a real repository has two levels.
The root file is an index — it lists what's in the repository, what every package is, and a few rules that hold for everything. It's also where the line from Anthropic's sample root file belongs: run commands from the package directory rather than the root of the monorepo. What's important is that this file gets loaded in every directory during every session, so keep it short
The package file contains specifics — what the test command is, that the build requires an env var, that there's a generated directory that mustn't be edited by hand, and what local conventions are used. It lives next to the code it documents, it's owned by the team working on this package, and they review it in their PRs along with the change that made it outdated
So for any line, ask yourself: does this apply to more than one package? If it doesn't — it belongs in the package file, and has no business in the root. If it does, it goes in the root file, because you can't put it in 8 files if 5 of them will drift.
Nearest doesn't win, it just goes last
It's also worth noting that proximity isn't authority — every tool whose docs say anything about this implements merging rather than overriding. Claude Code concatenates everything it finds "rather than overriding each other", and when Codex's docs say the closer file overrides earlier guidance, it's because they include it later in the prompt they assemble.
We wouldn't call that a conflict-resolution strategy though — it's a hope about attention. Claude Code's docs are very explicit here: if two loaded files disagree, it may pick one arbitrarily.
The only exception we've found is Codex's AGENTS.override.md, which if present in a given directory makes Codex ignore the AGENTS.md file at this level.
That's why it's important to write the package file as an addition to the root file, not as a correction of it. If you catch yourself writing "unlike the root file…", it means you should remove that line from the root file.
The budget is shared
The two levels share the same context, and in one of these tools they share a hard limit as well. The root file eats first there, because it's read first:
Codex stops adding files once they weigh more than
project_doc_max_bytestogether, which is 32 KiB by default — so an oversized root file can push the package file out of the chainClaude Code says you should aim to keep every file under 200 lines
Antigravity has a limit of 12,000 characters per rules file
These are not targets to aim for, it's about the direction — every line in the root file is charged to every session in every package, and in at least one tool it might push out the very thing you wanted to load.
Check it from where you actually work
That's why it's worth verifying this from where you actually start your sessions. We already did the technique in the lesson on silently failing configs — create a token, put it in the file, then ask about it in a new session. In a monorepo, do it twice: once from the root of the repository and once from within the package. They're two different setups, and the one that matters is the one that matches how you start sessions.
And do it again if you move a file, rename a directory, or change your starting point — the entire maintenance process is less time-consuming than having the agent use a wrong test command for an afternoon.
In Claude Code
CLAUDE.md, per directory — and the documentation is specific about it working in both directions.
Above your working directory: it reads all of the ancestor files when you start Claude, from the repository root down through your current working directory
Below it: it pulls in descendant files lazily, only when it opens a file in that directory
That's exactly what we saw in our test — the package marker wasn't there at first, and appeared after it opened packages/api/src/index.ts.
The best structure would be sth like this:
monorepo/
CLAUDE.md # the map, plus what's true everywhere
packages/
api/
CLAUDE.md # commands, landmines, local conventions
.claude/skills/
web/
CLAUDE.mdThe second mechanism is that you can create .claude/rules/ files and define a paths: glob there, in which case they'll be activated for files matching that glob. For example:
---
paths:
- "packages/api/**/*.ts"
---
Database queries go through Knex in src/db/. Never write raw SQL in a route handler.The above will be activated only for files under packages/api/. Both of these mechanisms achieve the same goal from two different perspectives:
directory file — sits next to the code, belongs to that particular directory
rule — sits at the root of the project, belongs to whoever maintains
.claude/
In most cases we'd go with rules when you want to enforce one convention for paths spread across multiple packages.
The claudeMdExcludes property lets you exclude other teams' files from Claude. It's a glob that gets evaluated against absolute paths, and we think it'd be best to place it in .claude/settings.local.json so it's yours, personally:
{
"claudeMdExcludes": ["**/packages/web/**"]
}The thing is though that this is a static list rather than a per-task switch — if you want to focus on some other package, the best solution is to run Claude from there.
There are two caveats:
.claude/settings.jsondoesn't have cascade behaviour like memory files do, so if you place it at the root level, its settings will be used only when you start Claude from the root of your project — you'll need to make every package's settings file self-containedafter
/compact, the rootCLAUDE.mdis loaded from the disk and inserted again, but not the nested ones; those come back the next time Claude reads a file in that directory, so if you work across several packages in one session you might lose some rules halfway through
Also, if your repo is mixed-tool: Claude Code reads CLAUDE.md and not AGENTS.md, and in a monorepo it does that once per package instead of once for the entire repository. To address this, name every package's file CLAUDE.md, or keep AGENTS.md as the source of truth and place a one-line CLAUDE.md next to it that imports it with @AGENTS.md. If there's nothing Claude-specific to say in that package you can also symlink it.
Finally, if you're curious what's being loaded — /context shows it under Memory files; for the lazily loaded ones, the docs suggest the InstructionsLoaded hook, which records which instruction files were loaded, when and why.
In Codex CLI
The AGENTS.md files Codex CLI uses are the ones found above the current working directory in the repo (including the root), by traversing the repo downwards to the current directory.
The merge rule is that files get combined with an empty line between them, with files closer to the current directory sitting at the end of the prompt. So generally, files are merged from the root all the way to the working directory, but…
The most crucial part is that the traversal ends in the working directory — everything under this directory is not loaded. You can test it yourself: navigate to the root of a repo with two packages and ask it to open packages/api/src/index.ts. You'll see its shell step reporting that packages/api/AGENTS.md exists, and it will still return the package marker as unknown. What matters here is where you start, in other words what your current working directory is:
cd packages/api
codexRun it from inside packages/api and you get ~/.codex/AGENTS.md, then the repo's root file, then the package's file, in that order. Run it from the root of the repo and you only get the first two.
The only exception is the file called AGENTS.override.md, which doesn't append to the prompt but replaces the regular AGENTS.md at the same level. You should use this in cases when you need to override the rules of a vendored or legacy subtree, where the root file is completely wrong rather than just not relevant. In other cases, regular AGENTS.md files that get appended are the preferable way.
The last thing worth mentioning is that once the merged files reach the project_doc_max_bytes limit (32 KiB by default), no more of them are loaded. This is important because the root file is read first, so a too big one will both take up the context and may push the package's file beyond this limit.
In GitHub Copilot CLI
Two different mechanisms — in case of monorepos the second one is more important.
If you place AGENTS.md anywhere within the repo then it works, and the nearest file in the directory tree wins. For the first mechanism we are looking in these 4 places:
the root of the repository
the directory where the session is started
directories between them
directories along the path of every file it's working on
In that last case Copilot becomes more like Claude Code than Codex, at least from what's documented. If you want to check it yourself you need a headless run that reads a file, and that needs a flag which disables permission prompts — so we based this on the provider's docs and haven't tested it ourselves. What we did test was starting a session from the root of the repository without opening any file, and it returned the package marker as unknown, so nested files are not loaded at the beginning.
The second mechanism is about path-scoped instruction files placed in .github/instructions/ with their own YAML frontmatter defining when they apply. The starting directory doesn't matter that much, for example:
---
applyTo: "packages/api/**/*.ts"
---
Route handlers validate input at the boundary. Errors cross the API as codes, never messages.You can define multiple globs in one applyTo value just separating them with commas: applyTo: "**/*.ts,**/*.tsx". It's definitely more useful for monorepos — you can keep a versioned directory with these scoped files and thanks to that not care much where you start the session from. Be mindful though that these modular files are found in the standard locations but not in the directories in between them, so don't scatter them per package.
The main thing to be aware of is the precedence. The docs say Copilot combines all the instructions it finds and removes identical duplicates, but they don't define an order between these files — and they advise you to avoid conflicting instructions. What it means is that for a single rule there are 3 places where you can put it (AGENTS.md, .github/copilot-instructions.md, path-specific files) and no documented winner. So to summarise — give every rule its one place.
In Cursor
The same two solutions work for Cursor in a monorepo. You can have a nested AGENTS.md file, and it's automatically applied when working with files in that directory or its children. If you put several AGENTS.md files in the same repo, their instructions are combined with the parent directories' — the more specific ones take precedence where they overlap. As an example, here's a simple project structure:
project/
AGENTS.md # repository-wide
frontend/
AGENTS.md # applies to frontend/ and everything under it
backend/
AGENTS.mdIn our trial repo we ran cursor-agent from the packages/api dir and it started with both AGENTS.md-s loaded, before touching anything.
The other solution is .cursor/rules, where you can put .mdc files and define when they should be applied. The "Apply to Specific Files" mode — with a globs pattern set in the frontmatter — is what's relevant here, for monorepos. So you can create a rule like this:
---
description: API package conventions
globs: packages/api/**/*.ts
alwaysApply: false
---
Every endpoint validates input at the boundary. Database access goes through the repository layer.This is useful if you want to have a convention that covers files scattered across multiple packages, rather than one neat subdirectory.
With this kind of rule, it's not active until you touch a file that matches the glob, so "the convention was skipped" and "there's no such rule" look exactly the same from the outside. If you need a package's rules to always be active, either set alwaysApply or put them into the AGENTS.md file in this package and start sessions from there.
In Antigravity CLI
Antigravity CLI is different from the entries above in that rules apply to the entire workspace rather than to particular directories, and you bind them using patterns instead of placement. The places rule files live:
~/.gemini/GEMINI.mdfor global rules, which apply across all workspacesthe
.agents/rulesfolder (or, for legacy reasons,.agent/rules) for workspace-level rules, which apply within the workspace or git rootGEMINI.mdorAGENTS.mdat the root of the workspace, if you go for a single-file-per-repo setup — as the docs describe it, the agent parses theGEMINI.mdandAGENTS.mdin your currently active directory
To achieve per-subtree targeting you pick the rule's activation mode:
Manual — the rule applies when you @-mention it in the agent input
Always On — it's applied continuously
Model Decision — it's applied when the model decides it's relevant, based on its description
Glob — it's applied to files matching a particular pattern, e.g.
src/**/*.ts
For example, if you set up a monorepo with a single rules folder at the root and a rule file per package, each glob-bound to its path, that'd look like this:
mono/
AGENTS.md # the map, always in play
.agents/
rules/
api.md # Glob: packages/api/**
web.md # Glob: packages/web/**It's worth noting that there's a 12,000 character limit per rules file, which might be an issue if you wanted to move a big root-level file in one piece
To include another file, prepend its path with @, eg:
@filenameRelative paths are resolved relatively to the rules file itself, absolute ones — relative to the repository or workspace root
A caveat on checking your work. The canary doesn't work here: on 1.1.10 a headless -p run wouldn't quote its rule files back at all, not even the one at the workspace root. That doesn't mean they aren't loaded — as per the docs it rather parses and enforces them as constraints, which doesn't have to mean pasting them into the prompt, and headless mode might not carry workspace context anyway. So it's best to test the actual behaviour you want to make sure of.
In Kimi Code CLI
We will be focusing on how the tool works with the AGENTS.md file, which describes the instructions in a way that is agnostic to the particular tool (it's also used by Codex and Copilot). The tool looks for it in the following places:
globally, using the Kimi Code-specific location
$KIMI_CODE_HOME/AGENTS.md(which resolves to~/.kimi-code/AGENTS.mdif you didn't change the data root)in the home directory as
~/.agents/AGENTS.mdin the current project as
AGENTS.mdin the current project as
.kimi-code/AGENTS.md
If it comes to a monorepo, what matters is the directory in which the session is started, so you need to make sure you are in the proper subdirectory. We've created a repo with two packages and ran the probe:
if we run it from the root of the repository, only the
AGENTS.mdlocated there gets loaded and the package-level file is not recognisedif we run it from within
packages/api/, both files get loaded
That's why we would suggest starting sessions from inside the target package:
cd packages/api
kimiAlso, keep in mind that reading files in subdirectories under the working directory is not documented and we didn't see it happen at launch, so avoid a repository structure that depends on it. To make sure, insert some unique word in the package-level file and ask about it from your regular starting point.
But before that, just keep in mind that there are two tools in this family:
The one we described above, which is the new Kimi Code CLI on a
0.xversion — 0.31.1 in this caseAn older one, the Python-based
kimi-clion a1.xversion, which it replaced
They might look similar but are completely different. The above is specific to the new version.
Don't split before you have something to split
It's also not useful to divide things prematurely. If there's a single pitfall in a single package, it doesn't justify 8 files. Per-package files earn their keep when the package really has its own commands and its own pitfalls, and when somebody working on it will notice when the file becomes outdated.
Without that, splitting won't help — you won't solve the problem of outdated instructions, you'll only scatter them across 8 files that are consulted even less often than the root file.
The inverse happens too: you create a root-level rule, copy it into three package files, then change it in only two of them.
That's why it's worth asking, in both directions: does this apply to more than one package? If the answer is "no", it goes in the package file; if it's "yes" — in the root file.
In a monorepo, the instructions that don't load weigh as much as the ones that do. Getting it right is primarily about the starting point of your sessions, and only secondarily about what's in the files.
packages/api/AGENTS.md# packages/api
Only what's true in this package. Anything true of the whole repo belongs in the root AGENTS.md,
and anything that contradicts the root file is a bug in the root file.
## Commands
- Tests: `npm test` — run from this directory, not the repo root
- One test file: `npm test -- src/__tests__/routes/users.test.ts`
- Dev server: `npm run dev` (port 3001; needs a local Postgres, `docker compose up db`)
- Migrations: `npm run migrate`
## Landmines
- `src/db/schema.generated.ts` is generated from the migrations. Edit a migration and regenerate.
- The OpenAPI spec in `openapi.yaml` is the contract the web package builds against. Changing a
response shape here without changing it there is how staging breaks.
## Conventions
- Routes are one file per resource in `src/routes/`, each exporting an Express router.
- Database access goes through `src/db/`. No raw SQL strings in a route handler.
- Errors cross the API boundary as codes, never as messages — messages are assembled client-side.