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)

repo/ ├─ CLAUDE.md, AGENTS.md ← repo-wide rules └─ sites/ ├─ blog/CLAUDE.md, AGENTS.md ← blog rules ├─ shop/CLAUDE.md, AGENTS.md ← shop rules └─ shared/ ← shared components

Claude Code

  • StartupThe root CLAUDE.md
  • On touchLoads blog's CLAUDE.md when it reads, writes or edits a file in sites/blog/
  • Not yetThe rules of a folder it hasn't touched yet (shop) aren't loaded

Codex

  • StartupThe root AGENTS.md
  • Not guaranteedThe AGENTS.md in sites/blog/ isn't loaded automatically (Codex doesn't search below the directory it was launched in)
  • Not guaranteedThe AGENTS.md in sites/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.

  1. !

    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.

ItemClaude CodeCodex
Loaded at startupEvery CLAUDE.md and CLAUDE.local.md in the launch directory and all directories above itFrom 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 directoryNot loaded at startup; loaded when Claude reads, writes or edits another file in that folderNot 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 limitsUnder 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 passedNothing is added once the total exceeds 32 KiB (the project_doc_max_bytes default)
The other tool's rule fileReads 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 fromClaude CodeCodex
The repository rootOnly the root rules; each site's rules are added when Claude touches that site's filesOnly 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.

ItemClaude CodeCodex
Project settings file.claude/settings.json.codex/config.toml
Which folder's file is readThe one in the launch directory (primary working directory); unlike CLAUDE.md, it isn't inherited from parent foldersFrom the project root down to the launch directory (closer ones take precedence)
Relation to trustRules such as allow take effect after you trust the folder; deny and ask take effect immediatelyReads .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 checkClaude CodeCodex
Rules loaded at startupThe Memory files list in /contextAsk "Show which instruction files are active." (this is the AI's own report; for certainty, use the log below)
Rules loaded during the sessionNot shown under Memory files; when one loads, a Loaded line with its path appears in the terminalNone are added mid-session (same as above)
Keep a recordUse 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.

Rule documents
Explain the reasons and the steps 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 stopped
Tool guards
Stop an action before it happens Hooks and permission settings in the AI tool (called "tool guards" in this article). Each tool configures them differently, and they may not take effect depending on where you launch and the trust settings
Release check
The last stop, whichever tool did the work If anything outside the assigned folder changed, the release process itself stops. It doesn't depend on the AI tool

In 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 triedResult
The blog agent changed only blog filesPasses
Moved a shop file into the blog folderStops (the original shop file is listed)
Changed a file in the shared folderStops (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 shopStops (a version that only checks the committed diff misses this)
Deleted the scope fileStops

--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.

This check prevents mix-ups, not deliberate violations. If the AI rewrites .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.

ItemClaude CodeCodex
Block file writesAdd 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 commandsAdd Bash(command *) to deny, or return exit code 2 or a deny JSON from a PreToolUse hookThe rule prefix_rule(pattern=[...], decision="forbidden") (experimental) and PreToolUse hooks
Block every processThe 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 projectReads .claude/settings.json in the launch directory; deny takes effect even before you trust the folderReads 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.git and git@github.com:org/app.git are 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 config

No variable interpolation

docker compose config --no-interpolate

With --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   # PowerShell

These 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 /context for 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 Loaded line, the InstructionsLoaded hook)
  • 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 /context or Codex's list of active instruction files, and what Claude Code loaded mid-session with the Loaded line or the InstructionsLoaded hook
  • 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.

Sources