Vibe Coder #4.2: Project Rule Files – The Constitution at the Root, By-laws in the Folders
All four coding agents can read AGENTS.md, just not the same way: one goes quiet when it sees CLAUDE.local.md, one never walks into subfolders, one silently drops a mistyped rule. The rule file is the constitution; permission rules and hooks are what guard the door.
This post is also available in Turkish →
The previous chapter was about the spec: the document you write once instead of explaining yourself at the start of every session. It ended with an IOU: where that document lives on disk, which tool reads it and when, and which one wins when two files disagree. This chapter pays that debt. The subject is AGENTS.md vs CLAUDE.md, plus Cursor's .cursor/rules folder and Antigravity's rule files.
Let me state the metaphor up front, because the whole piece leans on it. The rule file at the root is the project's constitution. Rules in subfolders and rules tied to file types are the by-laws. But a constitution does not enforce itself. Someone else stands at the door, and the last part of this piece is about them.
What AGENTS.md is, and why it is not enough on its own#
AGENTS.md is a plain Markdown file that explains a project to coding agents; in its own site's words, "a README for agents". It no longer belongs to one company: the agents.md page says the format is stewarded by the Agentic AI Foundation under the Linux Foundation and is used by more than 60,000 open source projects. It came out of a collaboration between companies including OpenAI, Google, Cursor, Amp and Factory.
In 4.1 I said the file-name problem was largely over, since all four tools can read this file. True. But there is a wide gap between "can read" and "reads the same way". Four courts, one constitution, four sets of procedure:
- When does it read? At session start, or when the agent walks into that folder?
- How much does it read? Does it cut off somewhere, and does it tell you when it does?
- Who wins on a conflict? The nearer file, the farther one, or neither?
Everything below comes from each tool's own documentation as of 11 October 2026.
Four tools, four codes of procedure#
Claude Code: CLAUDE.md first, AGENTS.md if there is none#
Claude Code looks for CLAUDE.md in four places: the organisation's managed policy file, the user's ~/.claude/CLAUDE.md, the project's ./CLAUDE.md (or ./.claude/CLAUDE.md), and a personal, uncommitted CLAUDE.local.md. Files in your working directory and the directories above it load at launch; files in subfolders load once Claude reads, writes or edits a file there.
Two details matter. First, the files do not override each other, they are concatenated: ordered from the root down to the working directory, with the nearest one read last. On contradictions the documentation is blunt: if two instructions contradict each other, Claude may pick one arbitrarily. So there is no guarantee that the nearest file wins.
Second, AGENTS.md. Since v2.1.277 Claude Code reads it directly, on one condition: there is no CLAUDE.md, .claude/CLAUDE.md or CLAUDE.local.md in your working directory or above it. The hidden treasure: the third file on that list. The team works from AGENTS.md, and you create an uncommitted CLAUDE.local.md for your own notes. From that moment on, Claude on your machine stops reading the team's AGENTS.md. The documentation says so plainly and gives the fix: set "Project instructions" to claude-md-and-agents-md.
The by-laws live in .claude/rules/. Give a file a glob in its paths field and the rule loads only when Claude touches a matching file. There are @path/to/file imports too, up to four hops deep. The recommended size is under 200 lines per file.
Codex: root downwards, at most one file per directory#
Codex checks a global file under ~/.codex first: AGENTS.override.md if it exists, otherwise AGENTS.md. Then it walks from the project root towards the working directory and takes at most one file per directory: AGENTS.override.md, then AGENTS.md, then any fallback names you configured.
Files are joined root first, so the nearer one comes later and overrides what came before. The documentation states one more rule outright: Codex stops searching once it reaches your current directory. Technical detail: going by that description, if you start Codex at the project root, api/AGENTS.md never enters the chain, because the search runs from the root to where you stand, not below it. A subfolder's by-laws only take effect when you start from that subfolder. The combined size is capped at 32 KiB by default (project_doc_max_bytes); once it is reached, no further files are added.
The command the documentation suggests for checking what was loaded:
# From the project root: have Codex list the instructions it sees, in precedence order
codex --ask-for-approval never "Summarize the current instructions."
Cursor: .mdc rules and nested AGENTS.md#
Cursor keeps project rules under .cursor/rules and is strict about the extension: the file must be .mdc, and a plain .md file is ignored by the rules system. There are four types: always applied, applied intelligently (the agent decides from the description), applied to specific files (globs), and applied manually. The recommended size is under 500 lines.
AGENTS.md is supported at the root and in subfolders; nested files are combined with their parent directories, with more specific instructions taking precedence. There is an organisational layer as well: Team Rules → Project Rules → User Rules, and when guidance conflicts, earlier sources take precedence. In Cursor, the company's rule overrides your personal one, not the other way round.
As for the old .cursorrules file: Cursor's older documentation page described it as "still supported, but deprecated", and the current rules page does not mention it at all. Do not create one in a new project, and put the migration of old ones on the calendar.
Antigravity: any folder, but no rule without a trigger#
Antigravity accepts AGENTS.md, GEMINI.md or a .agents/rules/ folder in any subdirectory of the project. It walks up from the folder of the file being read or edited to the workspace root and loads the rules at each level. Rules are cumulative rather than replacing each other; on a conflict, the more specific directory rule wins. Global rules live under ~/.gemini/.
Every file in .agents/rules/ needs a trigger in its YAML frontmatter: always_on, model_decision, glob or manual. The hard truth: a rule with no frontmatter, or with an unrecognised trigger, is silently discarded. The documentation's own example is alwaysOn: capitalise one letter and the rule never loads, with no error. On the budget side, a single file is truncated at 24,000 bytes, and global plus always_on rules share a 20,000-token budget; whatever does not fit is demoted to a pointer.
Who wins on a conflict?#
Put the four side by side:
| Files it reads | Subfolders | On a conflict | Limit | |
|---|---|---|---|---|
| Claude Code | CLAUDE.md, CLAUDE.local.md, .claude/rules/; AGENTS.md if no CLAUDE.md |
When a file there is touched | All concatenated, nearest read last; the pick can be arbitrary | 200 lines per file recommended |
| Codex | AGENTS.override.md, AGENTS.md, fallback names | Only from root to working directory | Nearer comes later and overrides | 32 KiB combined |
| Cursor | .cursor/rules/*.mdc, AGENTS.md (root and subfolders) |
Nested AGENTS.md combined | More specific wins; Team → Project → User | 500 lines per rule recommended |
| Antigravity | AGENTS.md, GEMINI.md, .agents/rules/*.md |
From the file's folder up to the root | Cumulative; specific directory wins | 24,000 bytes per file, 20,000 tokens for always_on |
The AGENTS.md site's own rule is "the closest AGENTS.md to the edited file wins; explicit user chat prompts override everything". Read the table and you can see that it holds for most of these tools and not for one of them. The practical rule that follows: never write a subfolder rule that contradicts the one above it. A by-law may narrow the constitution; it should not pick a fight with it. "Use the mysql client at the root, only PDO under api/" is a narrowing. "Ask for approval at the root, do not ask under scripts/" is a contradiction, and one of the four tools will settle it arbitrarily.
The constitution at the root, by-laws in the folders#
The layout I recommend rests on a single source:
- AGENTS.md at the root: environment, prohibitions, approval gates, traps. The spec from 4.1 goes here.
- CLAUDE.md at the root, for Claude Code: a single
@AGENTS.mdline with Claude-specific additions below it. Use this instead of a symlink if anyone on the team is on Windows. - AGENTS.md in a subfolder: that folder's narrowings. Codex only sees them when started from that folder.
- Rules tied to file types, if you need them, in each tool's own format:
.claude/rules/*.md(paths),.cursor/rules/*.mdc(globs),.agents/rules/*.md(trigger: glob). This is the one place that genuinely needs a copy per tool; keep it short.
The most dangerous thing about this spread is that the failures are silent. A Cursor rule with the wrong extension, an Antigravity rule with a mistyped trigger, a Codex chain over 32 KiB, a CLAUDE.local.md created by accident: none of them raises an error. The agent simply never sees those lines. So instead of hunting for them by hand I wrote a small checker. No framework, plain PHP 8.3:
<?php
declare(strict_types=1);
/**
* Rule file audit: lists the cases the four tools swallow silently.
* Usage: php rule-audit.php /path/to/project
* Limits taken from each tool's own documentation (October 2026).
*/
$root = rtrim($argv[1] ?? getcwd(), '/');
$warnings = [];
$files = new RecursiveIteratorIterator(
new RecursiveCallbackFilterIterator(
new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS),
fn (SplFileInfo $f) => !in_array($f->getFilename(), ['.git', 'vendor', 'node_modules'], true)
)
);
foreach ($files as $file) {
$path = substr($file->getPathname(), strlen($root) + 1);
$name = $file->getFilename();
$bytes = $file->getSize();
if ($name === 'CLAUDE.md') {
$lines = count(file($file->getPathname()));
if ($lines > 200) {
$warnings[] = "$path: $lines lines. Claude Code recommends under 200 lines per file.";
}
}
if ((in_array($name, ['AGENTS.md', 'GEMINI.md'], true) || str_contains($path, '.agents/rules/')) && $bytes > 24000) {
$warnings[] = "$path: $bytes bytes. Antigravity truncates everything after 24,000 bytes.";
}
if (str_contains($path, '.cursor/rules/') && $file->getExtension() === 'md') {
$warnings[] = "$path: Cursor ignores plain .md files under .cursor/rules; the extension must be .mdc.";
}
if (preg_match('#(^|/)\.agents/rules/[^/]+\.md$#', $path)) {
$head = (string) file_get_contents($file->getPathname(), false, null, 0, 512);
if (!preg_match('/^---\R.*?^trigger:\s*(always_on|model_decision|glob|manual)\s*$/ms', $head)) {
$warnings[] = "$path: no valid 'trigger'. Antigravity silently discards this rule.";
}
}
}
// Codex joins the AGENTS.md chain from the root to the working directory; default cap 32 KiB.
$chain = 0;
foreach (glob($root . '/{AGENTS.md,AGENTS.override.md}', GLOB_BRACE) ?: [] as $f) {
$chain += filesize($f);
}
if ($chain > 32 * 1024) {
$warnings[] = "Root AGENTS files total $chain bytes. Codex stops loading at 32 KiB.";
}
// Claude Code: with a CLAUDE.local.md present, AGENTS.md is no longer read (default setting).
$hasClaude = is_file("$root/CLAUDE.md") || is_file("$root/.claude/CLAUDE.md");
if (is_file("$root/AGENTS.md") && !$hasClaude && is_file("$root/CLAUDE.local.md")) {
$warnings[] = 'CLAUDE.local.md present, no CLAUDE.md: with default settings Claude Code will not read your AGENTS.md.';
}
echo $warnings === [] ? "Clean.\n" : implode(PHP_EOL, $warnings) . PHP_EOL;
exit($warnings === [] ? 0 : 1);
On a deliberately broken test folder (a 40,000-byte AGENTS.md with a CLAUDE.local.md next to it, .cursor/rules/php.md, an Antigravity rule with trigger: alwaysOn, and a 250-line api/CLAUDE.md) it raised all six warnings and exited with 1; on a clean folder it printed "Clean." and returned 0. The exit code means you can hang it on CI. Its scope is narrow: it sums only the root, not Codex's subfolder chain, and it does not climb into Claude Code's parent directories. It is an inspector, not a judge.
A constitution does not enforce itself#
4.1 carried a warning; here is what it looks like in practice. Claude Code's documentation says these files load as context, not as enforced configuration. "No deletion without approval" is written into the constitution; whether the agent reads it and complies is down to good faith. The guard at the door is a separate thing, and it has a different name in every tool:
- Claude Code:
permissionsrules insettings.json(evaluated deny first, then ask, then allow) and thePreToolUsehook. - Codex:
.rulesfiles written in Starlark under~/.codex/rules/, withallow,promptandforbidden. The documentation flags these as experimental. - Cursor: a
beforeShellExecutionhook in.cursor/hooks.json; if it returnsdenyor the script exits with 2, the command does not run. - Antigravity: rules in the permission settings, checked Deny → Ask → Allow, plus the Default, Request Review and Turbo presets.
The approval gate from 4.1 ("before you change data: count, report, wait") looks like this at the door, on the Claude Code side:
#!/bin/bash
# .claude/hooks/approval-gate.sh — Claude Code PreToolUse hook.
# Stops SQL that deletes data; hands the reason back to Claude via stderr.
CMD=$(jq -r '.tool_input.command // empty')
if echo "$CMD" | grep -qiE '(DROP[[:space:]]+(TABLE|DATABASE)|TRUNCATE([[:space:]]+TABLE)?[[:space:]]+[a-z_]|DELETE[[:space:]]+FROM)'; then
echo "Blocked: SQL that deletes data. Run COUNT(*) first, report the affected rows, wait for approval." >&2
exit 2
fi
exit 0
You wire it to the PreToolUse event in .claude/settings.json with a Bash matcher. I ran the script against sample inputs: DELETE FROM and TRUNCATE stopped with exit code 2, SELECT COUNT(*) went through. My first version matched the bare word DROP, and it stopped git log | grep -i drop-shadow as well. A hook that hunts for words turns back innocent travellers too. Claude Code's documentation points the same way: the hook's command filter is best effort, so use the permission system for a hard allow or deny.
On Codex the same idea becomes a prefix rule. The match and not_match examples are validated when the file loads, so a badly written rule tells you straight away:
# ~/.codex/rules/default.rules (Starlark)
prefix_rule(
pattern = ["git", "push"],
decision = "prompt",
justification = "Ask for approval before pushing to a remote",
match = ["git push origin main"],
not_match = ["git status"],
)
To test it, codex execpolicy check --pretty --rules ~/.codex/rules/default.rules -- git push origin main shows the decision and the rule that matched. I did not run this rule or the command on Codex myself; the syntax follows the documentation's example.
The point: put the "why" in the rule file and the "what" at the door. The constitution tells the agent why it should stop; the hook stops it on the day it does not.
When it helps, and when it does not#
It helps: when the team uses more than one tool; when folders in a monorepo genuinely have different rules; when the root spec starts pushing against the budgets from 4.1.
It does not: on a one-person project with one tool, a single AGENTS.md at the root is enough and you need no by-laws. Scattering files into every folder adds confusion, not clarity; three months later you will not know where a rule came from either.
In the chapter on the tools (in Turkish) I said how to write each of these files was a job for 4.2; that account is now settled too. In the next chapter, Vibe Coder #4.3: Iterative Development, we look at how to cut the work into small pieces underneath these rules. The full series index lives in the Vibe Coder's Handbook (in Turkish).
Writing a constitution is easy. A project that puts no guard at the door leaves its constitution to the agent's conscience. Stay with the technology.
Comments
0 comments
Leave a comment