Your AGENTS.md is on disk. Prove it reached the model before the agent job runs
For five days in September, Claude Code sessions with telemetry turned off skipped AGENTS.md without a warning. Version 2.1.281 fixed that, but the failure mode stays. An instruction file that never loaded looks exactly like a model ignoring its rules. A free /context call that lists what loaded, a canary check that costs a fraction of a cent and proves each file's text arrived, four ways that check passes when it should fail, nine configurations measured on Claude Code 2.1.286, and the 32 KiB cut Codex makes to the file closest to your working directory.
Claude Code 2.1.277, released on 18 September, started reading AGENTS.md as project instructions in repositories that have no CLAUDE.md. Until 2.1.281 shipped on 23 September, some sessions read CLAUDE.md files only and skipped AGENTS.md with nothing on screen to say so. That included sessions with telemetry turned off, where even DISABLE_TELEMETRY=0 counts as off, and sessions on Amazon Bedrock. Przemek Szypowicz traced the gate in the 2.1.280 bundle to a remote feature flag, tengu_agents_md_mod, which falls back to false when the flag cannot be fetched. The 2.1.281 changelog reads: "Changed AGENTS.md support to also work on Amazon Bedrock, Google Vertex AI, Microsoft Foundry, LLM gateways, and sessions with telemetry disabled." On 2.1.281 or later, that bug is fixed.
The failure mode is still there. A CI job on those versions that relied on AGENTS.md alone, with telemetry off, ran its agents without the project rules. In issue #95690 one reporter points out that turning flag fetching off is "exactly what CI runners do". The symptom looks like a model ignoring instructions, so the obvious response is to rewrite the instructions, and that changes nothing if the file is never read. A file on disk tells you nothing about what the model received. Most of the ways an instruction file goes missing are documented behavior, not bugs, so no release will remove them. The two checks below find them in a few seconds and can run as the first step of any agent job.
Ask Claude Code which files it loaded
In an interactive session, /context shows a Memory Files table. The same command works in print mode, and it never calls the model:
claude -p "/context" < /dev/null | grep -A6 "### Memory Files"
### Memory Files
| Type | Path | Tokens |
|------|------|--------|
| Project | /private/tmp/canary/t1/AGENTS.md | 23 |
With --output-format json, the same run reports total_cost_usd: 0 and num_turns: 0. When no instruction file loads, the section is not printed at all, so the grep exits non-zero and the job can stop there. One version caveat from the memory docs: before 2.1.280, /context did not list an AGENTS.md that Claude read directly, so on older versions this check reports a miss for a file that did load.
Keep the < /dev/null in scripts. Without it, claude -p waits three seconds for stdin and prints a warning before it carries on.
A canary proves the text arrived
/context lists paths. It does not show what was left of each file when it reached the model, and text does get removed on the way. Claude Code strips block-level HTML comments from CLAUDE.md before it injects the file, so a rule written inside <!-- --> never reaches the model. Codex cuts project instructions at a byte budget, covered below. And /context is a Claude Code command, so other agents need a check that does not depend on it.
A canary covers all three cases. Add one line at the end of each instruction file, with a suffix nobody could guess:
Canary: CANARY-AGENTS-7Q2X
Put it at the end because truncation removes the end. Give each file its own suffix (CANARY-ROOT-…, CANARY-API-…) so the result tells you which files arrived. Then ask the model for every word that starts with the prefix, with no tools available, so the context is the only place it can find the answer:
#!/usr/bin/env bash
# check-instructions.sh: fail when a canary from an instruction file did not reach the model.
# Usage: ./check-instructions.sh CANARY-AGENTS-7Q2X [CANARY-API-...]
set -euo pipefail
expected=("$@")
out=$(claude -p "List every word in your context that starts with CANARY-. Reply with only those words, one per line, or NONE." \
--model haiku --tools "" --strict-mcp-config < /dev/null)
for word in "${expected[@]}"; do
if ! grep -qx "$word" <<< "$out"; then
echo "missing from context: $word" >&2
echo "model saw: $out" >&2
exit 1
fi
done
echo "instructions loaded: ${expected[*]}"
In a fresh repository with only that AGENTS.md, the script prints instructions loaded: CANARY-AGENTS-7Q2X. After a one-line CLAUDE.local.md is added next to it, the same run prints missing from context: CANARY-AGENTS-7Q2X and model saw: NONE, and exits 1. A CLAUDE.local.md counts as a CLAUDE.md, and any CLAUDE.md in the working directory or above it switches AGENTS.md off. Someone who adds one for private notes in a repository that runs on AGENTS.md loses the shared rules without noticing.
On Haiku 4.5 the check takes one turn and about 6,700 tokens of context. Claude Code estimates the cost at $0.003.
Four ways the check fools you
Tools left on. In a repository where AGENTS.md had not loaded, the prompt "What is the canary word in AGENTS.md?" with default tools returned the correct word in two turns, because the agent opened the file from disk. The check passed and proved nothing. With --tools "", the same prompt returned no word. Leave the file name out of the prompt, and take the tools away.
MCP tools survive --tools "". That flag covers the built-in tools only. On a machine with MCP servers configured, the tool-less run still took three turns while the model searched for some other way to read the file. With --strict-mcp-config and no --mcp-config, no MCP server is loaded, and the same run took one turn.
--tools "" placed before the prompt swallows it. --tools accepts several values, so claude -p --tools "" "List…" reads the prompt as a tool name and exits with Input must be provided either through stdin or as a prompt argument when using --print. Put the prompt first, as the script does.
A guessable canary. If the word can be inferred from the prompt or from the rest of the file, the model can produce it without having seen the line. The prompt names only the prefix, the suffix is random, and grep -qx accepts only an exact whole line.
Run it with the job's flags, from the job's directory
What loads depends on the flags, the directory the session starts in, and files that exist on one machine only. A check proves something only for the configuration it ran in, so copy the job's flags onto the claude line in the script and start it in the same directory as the job. Each row below ran twice on Claude Code 2.1.286 with Haiku 4.5, and both runs returned the same canaries.
| Repository contains | Extra flags | Canaries returned |
|---|---|---|
AGENTS.md | none | AGENTS.md |
AGENTS.md | --setting-sources project | AGENTS.md |
AGENTS.md | --setting-sources user | none |
CLAUDE.md | --setting-sources user | none |
AGENTS.md | --setting-sources user --append-system-prompt-file AGENTS.md | AGENTS.md |
AGENTS.md, CLAUDE.local.md, .claude/rules/style.md, sub/CLAUDE.md | none | rules file, CLAUDE.local.md |
| the same four files | --setting-sources project | rules file |
AGENTS.md, CLAUDE.local.md, and a CLAUDE.md holding @AGENTS.md | none | AGENTS.md, CLAUDE.local.md |
AGENTS.md, and a CLAUDE.md saying "Read AGENTS.md before you start any task." | none | none |
--setting-sources controls instruction files as well as settings.json. user is a common way to keep a repository's settings out of a scripted run, and it also drops that repository's CLAUDE.md and AGENTS.md. The seventh row is a combination the documentation does not describe: with project alone, CLAUDE.local.md is not loaded, yet its presence still keeps AGENTS.md out, and only the rules file is left.
sub/CLAUDE.md is missing from every row by design. Files in subdirectories load when Claude reads a file in that directory, not at session start. To check one, run the script from that subdirectory, where it becomes an ancestor and loads at launch.
The last two rows are the two common ways of pointing CLAUDE.md at AGENTS.md. The @AGENTS.md import loads the file every time. A sentence asking Claude to read it gives the model the file only if it decides to open it, which the docs also state.
Two more modes skip project instructions on purpose, and neither was run for this post because the test machine has no API key. --bare skips CLAUDE.md auto-discovery, and the headless docs say it "is the recommended mode for scripted and SDK calls, and will become the default for -p in a future release." When that default changes, a claude -p job that relies on discovery stops receiving the project rules. The fifth row shows the replacement that survives it: pass the file with --append-system-prompt-file, which the docs list among the flags bare mode accepts. --safe-mode also skips CLAUDE.md.
There is one more reason to run the check inside the job rather than once on a laptop. The env-vars docs say a flag-gated feature can be missing in the first session after Claude Code is installed or upgraded. On a runner that installs Claude Code fresh for every job, every session is a first session. In the issue thread, one reporter found AGENTS.md skipped in the very first session after a fresh install of 2.1.278, then loading once the flag was cached.
Codex cuts the closest file at 32 KiB, and only logs it
Codex reads at most one instruction file per directory, from the project root down to the working directory. In each directory it tries AGENTS.override.md first, then AGENTS.md, then any names in project_doc_fallback_filenames (docs). A leftover AGENTS.override.md therefore replaces the AGENTS.md next to it entirely.
The project files share a cap, project_doc_max_bytes, which is 32 KiB by default. The documentation disagrees with itself here: the AGENTS.md guide describes a combined limit, while the advanced configuration page says "how much to read from each AGENTS.md file". The source at tag rust-v0.159.0 (codex-rs/core/src/agents_md.rs) shows a single budget shared by every project file in the chain. A file that exceeds the remaining budget is truncated at that byte. The only signal is a log-level warning, "project doc exceeds remaining budget; truncating". Files are read root first, so the file cut short is the one closest to the working directory, which holds the most specific rules. The same function returns no project instructions at all for a project marked trust_level = "untrusted" in ~/.codex/config.toml.
For Codex, the canary at the end of the deepest file is the check that matters. codex exec has no switch that removes tools, and the read-only sandbox still lets the agent read files, so the check also fails if the agent did anything except answer:
codex exec --json --ephemeral -s read-only \
"List every word in your instructions that starts with CANARY-. Do not run any commands or read any files. Reply with only those words, one per line, or NONE." \
< /dev/null > canary.jsonl
if jq -e 'select(.type == "item.completed" and (.item.type | IN("agent_message", "reasoning") | not))' canary.jsonl > /dev/null; then
echo "the agent opened something instead of answering from context" >&2
exit 1
fi
jq -r 'select(.type == "item.completed" and .item.type == "agent_message") | .item.text' canary.jsonl \
| grep -qx "CANARY-AGENTS-7Q2X"
The flags come from codex exec --help on 0.159.0, and the event types (agent_message, reasoning, command_execution, mcp_tool_call) come from codex-rs/exec/src/exec_events.rs at the same tag. The jq logic was tested against JSONL in that shape, passing a clean run and failing one that contained a cat AGENTS.md command. The Codex login on the test machine had expired, so this variant was not run end to end against the model. If your files are near the cap, raising project_doc_max_bytes in config.toml lifts it.
What a passing check does not tell you
It tells you the text was in the context at the start of the session, nothing more.
- Whether the model follows the line. That is a separate question, and some lines make results worse. The "use TDD" line in your AGENTS.md scored below writing nothing covers how to test it. What belongs in the file is covered in How to write AGENTS.md.
- Whether it is still there an hour in. The memory docs list what can drop out after compaction: instructions given only in conversation, a nested
CLAUDE.mdthat has not reloaded yet, and a path-scoped rule that has not matched a file since. - Whether the next release loads it the same way. The fix in 2.1.281 was one of 177 lines in that release's changelog. Read the release, not its summary for the method, and run the canary again after each upgrade of the CLI your jobs pin.
The canary line adds a few tokens to every call. If you trim scripted runs with the flags in Measure what your agent harness sends on every call, run the check with those same flags, because --setting-sources is one of them and it decides which instruction files load.
Get the next post when it ships
One email on Sunday with the new post and a short list of what shipped that week — new guides, tool updates, and a couple of links worth reading.