Put several sites in one repository, drop AI rules (CLAUDE.md or AGENTS.md) into each site's folder, and the AI will follow that site's rules—that is the natural assumption, but neither Claude Code nor Codex "always reads" the rules in lower folders. When you launch from the repository root, Claude Code doesn't load them until it touches files in that folder, and Codex doesn't load them automatically (the AI may open them on its own, but there's no guarantee). This article compares, item by item and based on both tools' official documentation, when these files are loaded and how to check whether they were, and then shows how to enforce the rules with automated checks instead of documents. It grew out of what we learned while designing this setup in an environment where AI agents run server operations and several projects.
When are the rules in each site's folder loaded? (launched from the repository root)
Claude Code
- StartupThe root
CLAUDE.md - On touchLoads blog's
CLAUDE.mdwhen it reads, writes or edits a file insites/blog/ - Not yetThe rules of a folder it hasn't touched yet (shop) aren't loaded
Codex
- StartupThe root
AGENTS.md - Not guaranteedThe
AGENTS.mdinsites/blog/isn't loaded automatically (Codex doesn't search below the directory it was launched in) - Not guaranteedThe
AGENTS.mdinsites/shop/(same)
So launch site work from the site's folder, and don't rely on documents for what must be enforced. Run an automated check that "stops the release if anything outside the assigned folder changed," and define each agent's scope with a per-worktree setting. Keep the rule documents to tell the AI the reasons and the steps.
Sources: Anthropic's How Claude remembers your project and OpenAI's Custom instructions with AGENTS.md (both checked November 1, 2026).
1. What we learned from putting rules in each site's folder
In an environment where AI agents handle server operations and several sites, we considered a design that puts several sites in one repository and places each site's rules in that site's folder (details that would identify locations or names are withheld, and the technical content is generalized). The goal was a division of labor: the AI assigned to blog reads only blog's rules and never touches shop.
But when we checked both tools' official documentation, the rules we placed weren't guaranteed to be loaded before work began. Which rules are loaded depends on which folder you launch from. On top of that, Claude Code's documentation describes CLAUDE.md as context, not enforced configuration. Even when the rules are loaded, nothing guarantees they'll be followed.
- !
Placing rules in folders isn't enough to divide responsibilities
- Risk
- An AI launched at the root changes files without having read that site's rules, or before reading them
- Fix
- Launch site work from the site's folder, and stop out-of-scope changes with a release check (section 4)
- Check
- Use the tools to confirm which rules were loaded (section 3), and deliberately change something out of scope to see whether the release stops
The general causes of AI ignoring rules, and their fixes, are covered in "Why AI Ignores Rules: Checking CLAUDE.md, Cursor Rules, and AGENTS.md." This article focuses on what happens when you split rules across folders. Two other cautions from the same environment, about handing several repositories to an AI, are in section 5.
2. When are per-folder rules loaded?
Whether the rules in each site's folder are loaded depends on which folder you launch from and which tool you use. Here is what both tools' official documentation says, side by side.
| Item | Claude Code | Codex |
|---|---|---|
| Loaded at startup | Every CLAUDE.md and CLAUDE.local.md in the launch directory and all directories above it | From the project root (usually the git root) down to the launch directory, one file per directory: AGENTS.override.md, otherwise AGENTS.md |
| Below the launch directory | Not loaded at startup; loaded when Claude reads, writes or edits another file in that folder | Not loaded automatically (the search stops at the launch directory; the AI may open them on its own) |
| Added during a session? | Yes (rules from folders it touches are added later) | No (built once at the start of each run) |
| Size limits | Under 200 lines per file is the guideline (longer files are followed less reliably); for CLAUDE.md files in lower folders, anything past 40,000 characters per file (with a 200K-token context) is cut, so only the beginning is passed | Nothing is added once the total exceeds 32 KiB (the project_doc_max_bytes default) |
| The other tool's rule file | Reads AGENTS.md when there is no CLAUDE.md, .claude/CLAUDE.md or CLAUDE.local.md in the launch directory or above it (default) | Doesn't read CLAUDE.md (only names added to project_doc_fallback_filenames are read) |
Sources: Anthropic's How Claude remembers your project and Monorepos and large repos, and OpenAI's Custom instructions with AGENTS.md (checked November 1, 2026). Claude Code's limit for lower folders applies from v2.1.309. With a 1M-token context, it's 150,000 characters (200,000 for Opus 4.6 and Sonnet 4.6). Claude Code reads AGENTS.md directly from v2.1.277 onward; an AGENTS.md in a lower folder is read when that folder has no CLAUDE.md and Claude opens a file there with the Read tool. To have both read, set Project instructions in /config to claude-md-and-agents-md.
Where you launch changes which rules are loaded
Even in the same repository, the result depends on where you launch. Launch from a site's folder, and both tools read that site's rules from the start.
| Launch from | Claude Code | Codex |
|---|---|---|
| The repository root | Only the root rules; each site's rules are added when Claude touches that site's files | Only the root rules; the site rules aren't loaded automatically |
sites/blog/ | Reads blog's and the root rules at startup (editing files outside the launch directory asks for approval) | Reads root, then blog; shop's rules aren't on the path, so they aren't loaded automatically |
The easy thing to miss is what counts as "touching" in "loaded on touch." What Claude Code's documentation counts as reading is reading, writing or editing a file with its tools, plus commands that display a single file, such as cat or head. Operations that change files in other ways, such as a bulk find-and-replace across several sites' folders, aren't on that list. A site's files can change before its rules are read.
In Codex, if you launch from the repository root and ask it to "fix both blog and shop," neither site's AGENTS.md is loaded automatically. The documentation says Codex stops searching when it reaches the current working directory, so a lower folder's rules are reliably included at startup only when you launch from that folder or below it.
That said, the default instructions (base_instructions/default.md) in the Codex repository tell the model, in effect, to check the applicable AGENTS.md files when working below or outside the current working directory. The AI may open a site's AGENTS.md on its own, but that's the AI's judgment, not a guarantee from the loading mechanism.
Per-folder settings files are inherited differently, too
Beyond rule documents, the tools also differ in how they handle folders for settings files such as permissions.
| Item | Claude Code | Codex |
|---|---|---|
| Project settings file | .claude/settings.json | .codex/config.toml |
| Which folder's file is read | The one in the launch directory (primary working directory); unlike CLAUDE.md, it isn't inherited from parent folders | From the project root down to the launch directory (closer ones take precedence) |
| Relation to trust | Rules such as allow take effect after you trust the folder; deny and ask take effect immediately | Reads .codex/ only in trusted projects |
Sources: Anthropic's Claude Code settings and Monorepos and large repos, and OpenAI's Config basics (checked November 1, 2026).
3. Checking whether the rules were loaded
Once the rules are in place, check after launch whether they were actually loaded. The method differs by tool, and in Claude Code you look in different places for rules loaded at startup and rules loaded later.
| What to check | Claude Code | Codex |
|---|---|---|
| Rules loaded at startup | The Memory files list in /context | Ask "Show which instruction files are active." (this is the AI's own report; for certainty, use the log below) |
| Rules loaded during the session | Not shown under Memory files; when one loads, a Loaded line with its path appears in the terminal | None are added mid-session (same as above) |
| Keep a record | Use the InstructionsLoaded hook to log which rules were loaded, when and why (it doesn't fire for an AGENTS.md read directly) | Run codex -c log_dir=./.codex-log to write the TUI log (./.codex-log/codex-tui.log) and see which rules were loaded |
Sources: the troubleshooting section of Anthropic's How Claude remembers your project and "Verify your setup" in OpenAI's Custom instructions with AGENTS.md (checked November 1, 2026). Add the log folder to .gitignore so it stays out of git.
To test a newly added lower-folder rule file in Claude Code, follow the documentation's advice: create the rule file in the shell rather than having the AI write it, then have the AI read a file in that folder and see whether a Loaded line appears.
4. Enforce rules with automated checks, not documents
As section 2 showed, rules placed in folders may not be loaded, depending on where you launch and which tool you use. And even when loaded, nothing guarantees they're followed. So think of enforcement in three layers.
CLAUDE.md and AGENTS.md. They may not be loaded, and even when loaded, nothing guarantees they're followed. Still, the AI needs them to understand why something stoppedIn this environment, we made the bottom layer, the release check, the deciding safeguard. Whether you switch AI tools or a person does the work directly, everything goes through the same check.
Define each agent's scope with a per-worktree setting
Give each site its own worktree (git worktree), and at the top of the worktree place a small settings file (.agent-owner) listing the folders the agent working there may change. If you wrote the scope in a rule document inside the repository, it would be identical in every worktree and couldn't vary by location. Keep the settings file out of git (add it to .git/info/exclude), and write it when you create the worktree.
Note that you launch the AI not from the top of the worktree but from the site folder inside it. The top of the worktree is the same as the repository root, so launching there gives the same result as "the repository root" in section 2.
# From the main checkout (/srv/repo), create a worktree for sites/blog at /srv/work-blog
cd /srv/repo
git worktree add /srv/work-blog -b blog-work
printf 'sites/blog/\n' > /srv/work-blog/.agent-owner
echo '.agent-owner' >> .git/info/exclude
# Launch the AI from sites/blog/ inside the worktree
cd /srv/work-blog/sites/blog
claude # or codex for Codex
# Before releasing, run the check from the main checkout, at the top of the worktree
cd /srv/work-blog
bash /srv/repo/tools/check-owner.sh origin/main
Run the check script from the main checkout (where the AI doesn't work) or from CI, not the copy inside the worktree. If you run the copy in the worktree, an AI that rewrites the check script itself would get straight through.
Stop the release when anything outside the scope changes
Put the following check (tools/check-owner.sh above) at the start of the release process. It assumes releases are made only from commits: it stops if uncommitted changes remain, and it stops if the committed diff from the base (origin/main or similar) includes even one file outside the assigned folders. It reads .agent-owner, so run it at the top of the worktree.
#!/usr/bin/env bash
# Run at the top of the worktree before releasing: stop if anything outside the assigned folders changed
set -euo pipefail
base="${1:-origin/main}"
owner_file=".agent-owner" # at the top of the worktree, kept out of git
if [ ! -s "$owner_file" ]; then
echo "No scope file ($owner_file). Stopping the release" >&2
exit 1
fi
# Stop if uncommitted changes remain (release only from commits)
if [ -n "$(git status --porcelain)" ]; then
echo "There are uncommitted changes. Stopping the release" >&2
exit 1
fi
# --no-renames: count a move from another folder as both a deletion and an addition
# Append a trailing / to each scope entry (so sites/blog doesn't match sites/blog-old)
# Tested with gawk (Git Bash). BSD awk may fail when a -v value contains newlines
outside=$(git diff --name-only --no-renames "$base"...HEAD |
awk -v list="$(tr -d '\r' < "$owner_file")" '
BEGIN { n = split(list, own, "\n")
for (i = 1; i <= n; i++) if (own[i] != "" && substr(own[i], length(own[i])) != "/") own[i] = own[i] "/" }
{ ok = 0; for (i = 1; i <= n; i++) if (own[i] != "" && index($0, own[i]) == 1) ok = 1
if (!ok) print }')
if [ -n "$outside" ]; then
echo "Files outside the scope have changed. Stopping the release:" >&2
echo "$outside" >&2
exit 1
fi
echo "Only in-scope changes"
These are the results when Claude Code, which wrote this article, built a test repository with two sites and a shared folder and ran the check on November 1, 2026, with Git 2.56 (Git Bash on Windows).
| What we tried | Result |
|---|---|
| The blog agent changed only blog files | Passes |
| Moved a shop file into the blog folder | Stops (the original shop file is listed) |
| Changed a file in the shared folder | Stops (the shared folder is out of scope unless listed) |
Wrote the scope without a trailing / and added sites/blog-old/ | Stops (a version that didn't append the / let it through) |
| Left uncommitted changes in shop | Stops (a version that only checks the committed diff misses this) |
| Deleted the scope file | Stops |
--no-renames is what keeps moves from slipping through. Without it, git lists a change it treats as a move under the destination name only, so a change that brings files over from another site goes unnoticed. The check also "stops" rather than "passes" when the settings file is missing, so that a worktree whose scope was never set doesn't get through.
.agent-owner, the check passes. To prevent that too, use the tool guards below to deny edits to this settings file as well.Add tool guards
Stopping only before release means you find out after the AI has already rewritten something out of scope. To stop it before the write, add the tools' own features.
| Item | Claude Code | Codex |
|---|---|---|
| Block file writes | Add Edit(path) to deny in the permission settings (applies to built-in tools and to commands Claude Code recognizes, such as sed, tee and redirects) | In a PreToolUse hook, inspect where apply_patch (also matched as Edit and Write) writes, and return deny as the permissionDecision |
| Block commands | Add Bash(command *) to deny, or return exit code 2 or a deny JSON from a PreToolUse hook | The rule prefix_rule(pattern=[...], decision="forbidden") (experimental) and PreToolUse hooks |
| Block every process | The sandbox (deny doesn't stop a script that opens and writes files itself) | The sandbox (sandbox_mode = "workspace-write" limits writes to the launch directory; writable roots add writable locations, so don't add them casually) |
| Conditions when placed in the project | Reads .claude/settings.json in the launch directory; deny takes effect even before you trust the folder | Reads the project's .codex/ only when the project is trusted; hooks don't run until you review and trust them |
Sources: Anthropic's Configure permissions and Hooks guide, and OpenAI's Hooks and Rules (checked November 1, 2026).
In Claude Code, settings that keep the blog worktree from changing the scope file (.agent-owner), the check scripts (tools/), the root rules, shop and the shared folder look like this (assuming the worktree is at /srv/work-blog). Paths starting with // are absolute paths. Worktree locations differ from machine to machine, so add these to that machine's user settings (~/.claude/settings.json) when you create the worktree, not to settings checked into the repository.
{
"permissions": {
"deny": [
"Edit(//srv/work-blog/.agent-owner)",
"Edit(//srv/work-blog/tools/**)",
"Edit(//srv/work-blog/CLAUDE.md)",
"Edit(//srv/work-blog/AGENTS.md)",
"Edit(//srv/work-blog/sites/shop/**)",
"Edit(//srv/work-blog/sites/shared/**)",
"Edit(//srv/repo/**)"
]
}
}
In Codex, register the check script under PreToolUse in the hooks configuration (your personal ~/.codex/hooks.json, the repository's .codex/hooks.json, or similar), with apply_patch as the matcher. Hooks don't run until you review and trust them with /hooks in Codex (if you change them, they need to be trusted again).
{
"hooks": {
"PreToolUse": [
{ "matcher": "^apply_patch$",
"hooks": [ { "type": "command", "command": "python3 ~/.codex/hooks/owner_guard.py" } ] }
]
}
}
The check script extracts the target paths from the *** Add File:, *** Update File:, *** Delete File: and *** Move to: lines of the apply_patch input, and returns deny if any is out of scope or protected. Here is a minimal example.
import json, posixpath, re, sys
# Codex PreToolUse hook: stop apply_patch if it changes out-of-scope or protected files
OWN = ["/srv/work-blog/sites/blog/"] # scope (include the trailing /)
PROTECT = ["/srv/work-blog/.agent-owner", "/srv/work-blog/tools/"]
event = json.load(sys.stdin)
command = event.get("tool_input", {}).get("command", "")
if isinstance(command, list):
command = "\n".join(command)
bad = []
for m in re.finditer(r"^\*\*\* (?:Add File|Update File|Delete File|Move to): (.+)$", command, re.M):
path = posixpath.normpath(posixpath.join(event.get("cwd", ""), m.group(1).strip()))
if not any(path.startswith(o) for o in OWN) or any(path.startswith(p) for p in PROTECT):
bad.append(path)
if bad:
print(json.dumps({"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Out-of-scope files: " + ", ".join(bad)}}, ensure_ascii=False))
Claude Code, which wrote this article, ran this script on hand-built inputs in the format Codex passes (in scope, shop, .agent-owner, a move into shared, and a deletion under tools/) and confirmed that only the in-scope change passed. We haven't run it inside Codex.
Bash commands can be written in many ways, so a hook can't fully determine where they write. OpenAI itself calls hooks "a useful guardrail, not a complete enforcement boundary." Block commands you want to stop, such as rm, with a rule using decision = "forbidden", and let the release check catch the rest. Rules go in the rules/ folder of a config layer, such as ~/.codex/rules/default.rules. A project's .codex/rules/ is read only when that project is trusted.
Tool guards must be rewritten for each tool, and they may not take effect depending on where you launch and the trust settings. So build on the release check, which every change must pass in the end, and layer tool guards on top to catch problems earlier. For writing Claude Code hooks, see "What Are Claude Code Hooks?"; for read-only settings, see "How to Use Codex and Claude Code Read-Only."
5. Also watch out for: mixing up repositories and environment variables
In the same environment, two other things happened when an AI was handed several repositories. Both were caught before they became serious incidents, but apart from how you place the rules, make procedures for them.
Tell similarly named repositories apart by their remote URL
An AI decided that the repository of a different service with a very similar name was "an old copy of the same service" and moved it elsewhere (it was restored right away, with its contents unchanged). Its reasoning was "no local changes, and the last update is old," but that only shows "nothing is mid-change" and "nobody has edited it recently." The more stably a service runs, the less often it's updated.
Before moving or deleting anything, run the following in both the target folder and the folder used in production, and compare the results side by side.
# Remote URL (strip any token or password embedded in the URL before displaying it)
git -C <folder> remote get-url origin | sed -E 's#://[^/@]+@#://#'
# First check that it isn't a shallow clone (if true, the next result is unreliable)
git -C <folder> rev-parse --is-shallow-repository
# Root commit (copies that branched from the same history share it)
git -C <folder> rev-list --max-parents=0 HEAD
- Compare remote URLs by host and repository path:
https://github.com/org/app.gitandgit@github.com:org/app.gitare the same repository written differently; conversely, with mirrors or host aliases, the same repository can look different - In a shallow clone, the root commit isn't the real first commit: in a clone fetched with
--depth 1, you get the oldest commit fetched (in our test, HEAD itself) - Don't display tokens embedded in the URL: a remote URL can contain an authentication token, and printing it as is leaks a secret into the output, just like the environment variables below
Even if the comparison shows it's "an unused copy," don't delete it right away; move it somewhere else first. Write down where it went, where it was and the command to put it back, and delete it only after confirming for a while that nothing has broken. In this environment, the mix-up could be undone right away precisely because the folder was moved, not deleted.
Look only at environment variable names
An AI ran a command to inspect a container's configuration that printed the environment variables with their values, and a database connection string (including the password) appeared in the output. Once a value appears in the output, it stays in the conversation log and is also sent to the model. If you only want to see what's set, the names are enough.
Container environment variables
Shows values (don't use)
docker inspect <container>
docker exec <container> printenv
docker exec <container> sh -c 'env | cut -d= -f1'Names only
docker inspect --format '{{range .Config.Env}}{{index (split . "=") 0}}{{println}}{{end}}' <container>
docker exec <container> awk 'BEGIN{for(k in ENVIRON) print k}'Without options, docker inspect prints the entire configuration, and its Env contains the values as is. In containers without awk, use docker inspect --format.
Docker Compose configuration
Shows values (don't use)
docker compose configNo variable interpolation
docker compose config --no-interpolateWith --no-interpolate, ${DB_URL} is shown as is, and env_file shows only the file path. Variables whose values are written directly in compose.yaml are shown.
Local shell
Shows values (don't use)
env
printenv
Get-ChildItem Env:Names only
compgen -e # bash
(Get-ChildItem Env:).Name # PowerShellThese are the results when Claude Code, which wrote this article, tested with dummy values on November 1, 2026, using Docker 29.8 and Docker Compose v5.6. In a container with one value containing newlines (the form used when passing the contents of a key file), running env | cut -d= -f1 printed the second and third lines of the value as is, mixed in with the variable names. awk's ENVIRON and docker inspect --format didn't show the values. docker compose config displayed the dummy values written in .env and env_file; with --no-interpolate, neither appeared, and only the value written directly in compose.yaml was shown.
To be sure, write "look only at environment variable names" in the rule documents, and also deny the commands that print values with the tool guards from section 4. If a value is printed anyway, treat it as "seen" and decide whether to rotate it. For a Codex setting that keeps environment variables themselves from being passed to the AI's commands, see section 6 of "How to Keep Codex From Reading Secret Files."
6. Checklist before handing over the work
- Decided that site work launches the AI from the site's folder inside the worktree
- After launch, confirmed the site's rules are included: Memory files in
/contextfor Claude Code, the list of active instruction files for Codex - For work launched at the root, prepared a way to see whether lower-folder rules were loaded mid-session (Claude Code's
Loadedline, theInstructionsLoadedhook) - Added a check that stops the release if anything outside the assigned folders changed, and watched it stop on a deliberate out-of-scope change
- Wrote each worktree's scope with trailing slashes, and made the check stop when the scope file is missing
- Denied writes outside the scope and to the scope file with tool guards
- Wrote into the rules a procedure to compare the remote URL and root commit with production before moving or deleting a folder, and to move rather than delete
- Wrote into the rules the commands that show only environment variable names, and denied the commands that print values with tool guards
Example text for the rule documents (ready to use)
## Rules when working with multiple projects
- This worktree's scope is only the folders listed in .agent-owner. Don't change files outside it
(changes there stop the release check)
- Before moving or deleting a folder, compare git remote get-url origin and
git rev-list --max-parents=0 HEAD with production (watch for shallow clones).
Move instead of deleting, and record how to restore
- Look only at environment variable names. Don't use env, printenv, docker inspect (without --format), or
docker compose config (without --no-interpolate)
Write the documents not as a means of enforcement but as an explanation, so that when something stops, the AI understands why and chooses the right procedure.
Summary
- When you launch from the root, rules placed in each site's folder aren't loaded by Claude Code until it touches files in that folder, and aren't loaded automatically by Codex (the AI may open them on its own, but there's no guarantee)
- In either tool, launching from the site's folder loads that site's rules from the start (with a worktree, launch from the site folder inside it)
- Check what was loaded at startup with Claude Code's
/contextor Codex's list of active instruction files, and what Claude Code loaded mid-session with theLoadedline or theInstructionsLoadedhook - Turn what must be enforced into an automated check that stops the release if anything outside the assigned folders changed, and layer tool guards on top to catch problems earlier
- Also, tell similarly named repositories apart by remote URL and root commit, and look only at environment variable names
FAQ
Q. Is a CLAUDE.md in a subdirectory loaded?
A. Yes, but not at launch. Claude Code loads a folder's CLAUDE.md when Claude reads, writes or edits a file inside that folder. At that point it doesn't appear under Memory files in /context; instead, a Loaded line with its path appears in the terminal. If you launch from that folder, it's loaded at startup and appears under Memory files.
Q. How do I get Codex to read an AGENTS.md in a subdirectory?
A. Launch Codex from that folder (in the CLI, codex --cd <folder>). At startup, Codex reads the AGENTS.md files from the project root down to the launch directory, and doesn't search below it. If you launch from the root, a lower folder's AGENTS.md is read if the AI opens it on its own, but the official documentation offers no setting that reliably loads it.
Q. In a monorepo, should I consolidate the rule documents into one file at the root?
A. Both tools' documentation recommends putting repo-wide rules at the root and site-specific rules in the site's folder. However, a site's rules may not be loaded unless you launch from that folder, so move anything that would cause trouble if ignored out of the documents and into the release check or tool guards.
Q. How do I get Claude Code to read both CLAUDE.md and AGENTS.md?
A. By default, if there is a CLAUDE.md, .claude/CLAUDE.md or CLAUDE.local.md in the launch directory or above it, AGENTS.md isn't read. Either import it with a single @AGENTS.md line in CLAUDE.md, or set Project instructions in /config to claude-md-and-agents-md.