AGENTS.md for Monorepos skill for Codex
Root rules once, package rules where they apply. Codex maps the workspace, prints the exact instruction chain each starting directory produces with a bundled script, then restructures root and package AGENTS.md files so shared rules appear once and package files carry only their deltas. It accounts for override shadowing, truncation at project_doc_max_bytes and sessions launched from the repo root.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- agents-md-monorepo Installs to ~/.agents/skills/agents-md-monorepo/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install agents-md-monorepo The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project agents-md-monorepo Installs to .agents/skills/agents-md-monorepo/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/agents-md-monorepo.zipunzip -o agents-md-monorepo.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) agents-md-monorepo PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $agents-md-monorepo, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs a layered set of nested AGENTS.md and AGENTS.override.md files for monorepos and multi-service repositories, so Codex gets repo-wide rules from the root and package-specific commands from the directory it works in, without duplicated or contradictory guidance or blowing the project_doc_max_bytes budget. Use when a monorepo has packages with different toolchains, when Codex applies root rules inside a package that needs other commands, or when debugging which AGENTS.md files Codex actually loaded.
Use it when
- A monorepo mixes toolchains or test commands per package
- Root guidance is long enough to risk truncating package files
- You need to know which instruction files Codex actually loaded
- A temporary AGENTS.override.md needs to be introduced or cleaned up
Use something else when
- The repo is a single package needing one good file: use agents-md-authoring
- Build orchestration itself is the problem: use monorepo-tooling
How to check the result
Run scripts/agents_chain.py for the root and each package directory, then confirm with codex debug prompt-input that package-specific text appears when starting inside that package.
The mistake it prevents
Assuming Codex loads a package's AGENTS.md when it was started at the repo root; only ancestors of the starting directory are read.
Done well, each directory Codex is started from yields a short, correct chain: universal rules once at the root, then only the deltas that matter for that package, with the exact commands that work there. Nothing is repeated, nothing contradicts, and the whole chain fits the byte budget.
Discovery rules you are designing for
| Rule | Consequence for layout |
|---|---|
Codex walks from the project root (first ancestor with .git, or project_root_markers) down to the working directory | Rules in sibling packages never load; rules in ancestors always load |
One file per directory: first non-empty of AGENTS.override.md, AGENTS.md, then project_doc_fallback_filenames | An override in a directory hides that directory’s AGENTS.md completely |
| Files are concatenated root first | Deeper files come later and win when they conflict |
Combined size capped by project_doc_max_bytes (32 KiB default) | Text past the cap is dropped, and deeper (more specific) files are the ones lost |
| Chain is built once per run, from the starting directory | Starting Codex at the repo root means package files are NOT loaded, even when it edits that package |
Global ~/.codex/AGENTS.md (or AGENTS.override.md) loads first | Personal preferences can leak into team runs; keep them minimal |
The fourth row is the important trap: because truncation removes the tail, a bloated root file starves the package files that carry the most specific instructions.
The fifth row is the second trap. When a task spans packages or Codex is launched
at the root, the root file must tell Codex where package rules live, for example: “Before
editing under packages/<name>/, read packages/<name>/AGENTS.md.”
Workflow
- [ ] 1. Map packages, toolchains and where people start Codex
- [ ] 2. Print the current chain for each starting directory
- [ ] 3. Decide what lives at root vs package level
- [ ] 4. Write or refactor the files
- [ ] 5. Re-run the chain check and a live load check
1. Map the repository
cat pnpm-workspace.yaml turbo.json nx.json lerna.json go.work Cargo.toml 2>/dev/null | head -80
git ls-files | grep -E '(^|/)(package.json|pyproject.toml|go.mod|Cargo.toml|pom.xml|build.gradle(.kts)?)$' | head -50
git ls-files | grep -E '(^|/)AGENTS(\.override)?\.md$'
cat .codex/config.toml 2>/dev/null # project_doc_* and project_root_markers can be set per project (trusted projects only)
Ask or infer where sessions start: repo root (common in the desktop app and CI), or inside a package (common for CLI users). Design for both.
2. Print the effective chain
Use the bundled script, which applies the documented lookup order, flags shadowed files and reports cumulative bytes:
python3 scripts/agents_chain.py packages/billing # chain for one starting dir
python3 scripts/agents_chain.py . --audit # every instruction file and what it shadows
python3 scripts/agents_chain.py apps/web --fallback TEAM_GUIDE.md --max-bytes 65536
It exits 1 when the project files exceed the budget. Pass --fallback for each name in
project_doc_fallback_filenames and --max-bytes when the limit is raised.
3. Decide placement
| Content | Place it in |
|---|---|
| Package manager, workspace-wide install, commit and PR rules, security boundaries | Root AGENTS.md |
| Map of packages and a pointer to package files | Root AGENTS.md (a 5 to 15 line table) |
| How to run one package’s tests, lint, build | That package’s AGENTS.md |
| Toolchain that differs from the root (Go service in a TS monorepo) | Package AGENTS.md, stating the commands in full |
| A rule that must REPLACE the parent’s rule for a subtree | State the replacement explicitly in the package file (“In this package use make test, not pnpm test”) |
| Temporary instructions during a migration or incident | AGENTS.override.md in the affected directory, with an owner and removal date inside it |
| Code review rules for one service | ## Code Review Rules section in that service’s AGENTS.md |
Prefer plain AGENTS.md files that add to their parents. Use AGENTS.override.md only
when a directory’s normal file must be suppressed; it does not suppress ancestor files.
To neutralise an ancestor rule, the deeper file has to contradict it in words, and the
later position makes it win.
4. Write the files
Root file pattern:
# AGENTS.md (repo root)
## Workspace
- pnpm workspaces + Turborepo. Install: `pnpm install --frozen-lockfile`.
- Run a task for one package: `pnpm turbo run test --filter=@acme/billing`.
- Full CI check: `pnpm turbo run lint typecheck test`.
## Packages
| Path | Stack | Instructions |
|---|---|---|
| `apps/web` | Next.js | `apps/web/AGENTS.md` |
| `services/billing` | Go 1.23 | `services/billing/AGENTS.md` |
| `packages/ui` | React library | inherits root rules only |
Before changing files under a path listed above, read its AGENTS.md if you did not
start in that directory.
## Boundaries
- Never edit another package's generated code; regenerate it.
- Cross-package changes: update the consumer and the provider in the same change.
Package file pattern (keep it to the deltas):
# services/billing/AGENTS.md
This package is Go, not TypeScript. Root pnpm commands do not apply here.
## Commands
- Test: `go test ./...` (one test: `go test ./internal/invoice -run TestRounding`)
- Lint: `golangci-lint run`
- Regenerate mocks: `go generate ./...`
## Rules
- Amounts are `int64` cents. Never use float64 for money.
- Migrations live in `migrations/`; never edit a merged migration.
5. Verify
python3 scripts/agents_chain.py <dir>for the root and every package with its own file: right files listed, no unexpectedignored:lines, total under budget.- Live check from a package directory, without a model call:
cd services/billing && codex debug prompt-input > /tmp/pi.json && grep -c "This package is Go" /tmp/pi.json - Behavioral check:
If the quoted rules come from the wrong file, look for a straycodex exec --cd services/billing --ephemeral "Which test command applies here? Quote your instructions."AGENTS.override.md(in the repo or~/.codex/) or aCODEX_HOMEpointing elsewhere.
Gotchas
- Codex launched at the repo root does not auto-load
packages/x/AGENTS.mdwhen it later editspackages/x. The root pointer table is what makes the agent go read it. - A nested git repository or submodule with its own
.gitbecomes its own project root; ancestors above it stop loading. Setproject_root_markersif that is wrong. project_root_markers = []makes the working directory the root, so only that one directory’s file (plus global) loads.- Raising
project_doc_max_bytesis a fix of last resort: every byte is spent on every turn. Shorten the root first. - Untrusted projects skip project
.codex/config.toml, so fallback names or byte limits set there are ignored until the project is trusted. Put them in~/.codex/config.tomlwhen they must always apply. - Overrides left behind after an incident keep hiding the real file. Grep for
AGENTS.override.mdin reviews and give each one an expiry line.
Output contract
**Chain check:** root (2.1 KB) -> services/billing (0.9 KB) = 3.0 KB of 32 KB
**Files changed:** AGENTS.md (cut from 410 to 120 lines), services/billing/AGENTS.md (new)
**Overrides:** none | services/x/AGENTS.override.md (owner, expiry)
**Verified:** prompt-input contains package rules from services/billing and apps/web What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "AGENTS.md for Monorepos"
short_description: "Root rules once, package rules where they apply"
default_prompt: "$agents-md-monorepo Codex keeps running pnpm test inside our Go service. Fix the AGENTS.md layout" scripts/agents_chain.py
#!/usr/bin/env python3
"""Show the AGENTS.md instruction chain Codex would assemble for a directory.
Mirrors the documented discovery rules: global file from CODEX_HOME
(AGENTS.override.md, else AGENTS.md), then one file per directory from the
project root down to the target directory, choosing the first non-empty file
among AGENTS.override.md, AGENTS.md and any fallback names. Reports sizes
against the project_doc_max_bytes budget and files shadowed by an override.
Usage:
python3 scripts/agents_chain.py [TARGET_DIR] [--fallback NAME ...]
[--max-bytes N] [--markers .git,.hg]
[--codex-home DIR] [--audit]
Exit codes: 0 ok, 1 project instructions exceed the budget, 2 usage error.
"""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
PRIMARY = ["AGENTS.override.md", "AGENTS.md"]
SKIP_DIRS = {".git", "node_modules", ".venv", "venv", "dist", "build", "target", ".next", "__pycache__"}
def non_empty(path: Path) -> bool:
try:
return path.is_file() and path.read_text(encoding="utf-8", errors="replace").strip() != ""
except OSError:
return False
def pick(directory: Path, names: list[str]) -> tuple[Path | None, list[Path]]:
"""Return the file Codex would use in this directory and the non-empty files it ignores."""
chosen = None
shadowed = []
for name in names:
candidate = directory / name
if non_empty(candidate):
if chosen is None:
chosen = candidate
else:
shadowed.append(candidate)
return chosen, shadowed
def find_root(target: Path, markers: list[str]) -> Path | None:
if not markers:
return target
for directory in [target, *target.parents]:
if any((directory / m).exists() for m in markers):
return directory
return None
def chain_dirs(root: Path | None, target: Path) -> list[Path]:
if root is None:
return [target]
rel = target.relative_to(root)
dirs = [root]
current = root
for part in rel.parts:
current = current / part
dirs.append(current)
return dirs
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("target", nargs="?", default=".", help="directory Codex is started from (default: cwd)")
parser.add_argument("--fallback", action="append", default=[], help="name from project_doc_fallback_filenames (repeatable)")
parser.add_argument("--max-bytes", type=int, default=32768, help="project_doc_max_bytes (default 32768)")
parser.add_argument("--markers", default=".git", help="comma-separated project_root_markers; empty string = cwd is root")
parser.add_argument("--codex-home", default=os.environ.get("CODEX_HOME", str(Path.home() / ".codex")))
parser.add_argument("--audit", action="store_true", help="also list every instruction file under the root and what shadows it")
args = parser.parse_args()
target = Path(args.target).resolve()
if not target.is_dir():
print(f"error: {target} is not a directory", file=sys.stderr)
return 2
names = PRIMARY + args.fallback
markers = [m for m in args.markers.split(",") if m]
root = find_root(target, markers)
print(f"target: {target}")
print(f"project root: {root if root else '(none found; only the target directory is checked)'}")
print(f"lookup order: {', '.join(names)}")
print()
global_file, global_shadowed = pick(Path(args.codex_home).expanduser(), PRIMARY)
if global_file:
print(f"[global] {global_file} {global_file.stat().st_size} B")
for s in global_shadowed:
print(f" ignored: {s} (an override is present)")
else:
print(f"[global] none in {args.codex_home}")
total = 0
over = False
order = 0
for directory in chain_dirs(root, target):
chosen, shadowed = pick(directory, names)
if chosen is None:
continue
order += 1
size = len(chosen.read_bytes())
total += size
flag = ""
if total > args.max_bytes and not over:
over = True
flag = " <-- budget reached here; later text is dropped"
print(f"[{order}] {chosen} {size} B (cumulative {total} B){flag}")
for s in shadowed:
print(f" ignored: {s}")
print()
print(f"project instructions: {total} B of {args.max_bytes} B budget")
if args.audit and root is not None:
print("\naudit of instruction files under the root:")
for dirpath, dirnames, _ in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
chosen, shadowed = pick(Path(dirpath), names)
if chosen:
rel = Path(dirpath).relative_to(root) or Path(".")
line = f" {rel}/{chosen.name}"
if shadowed:
line += " (shadows " + ", ".join(s.name for s in shadowed) + ")"
print(line)
if over:
print("FAIL: combined project instructions exceed the budget", file=sys.stderr)
return 1
print("OK")
return 0
if __name__ == "__main__":
sys.exit(main())
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.