[All writing]

The Context Engine: CLAUDE.md, AGENTS.md, and the Tri-Layer Spec

CLAUDE.md, AGENTS.md, PRODUCT.md, DESIGN.md, and BRAND.md are context an agent reads. A hook is what blocks an action, and that split decides what goes in each file.

Jeffrey Liu9 min read

CLAUDE.md and AGENTS.md are context that Claude reads at the start of every session, and nothing in them can block an action, which is the job of a hook.

Anthropic's documentation states this directly. The memory page says Claude "treats them as context, not enforced configuration," and that to block an action regardless of what Claude decides you should use a PreToolUse hook instead. Everything you write in CLAUDE.md sits in the context window beside the work, and nothing in it can stop an action from happening.

I run a strict Voice section in my project CLAUDE.md that lists every writing tell I want avoided, and it loads into every session. On 2026-09-20 the model still produced those tells repeatedly across one long session. The memory page says longer files consume more context and reduce adherence. Rewriting that section would not change how it loads, so I moved the rule into a hook.

What each of the five files can enforce

FileWhat it holdsCan it enforce?
CLAUDE.mdBuild commands, conventions, project layoutNo. Context only
AGENTS.mdThe same, in a cross-tool formatNo. Context only
PRODUCT.mdFeature scope, user paths, what is out of scopeNo
DESIGN.mdTokens, type scale, spacing, component rulesNo
BRAND.mdVoice, register, vocabulary that is off-limitsNo

Every row says no. These files shape what the model reaches for, and enforcement comes from hooks, linters, and tests.

1. CLAUDE.md

This is where you write down what you would otherwise retype, and the docs name two triggers for adding to it. Claude makes the same mistake a second time, or you type the same correction you typed last session.

Two constraints apply. Keep the file under 200 lines, since it loads into context at the start of every session and competes with your actual work for room. When it grows past that, move file-specific guidance into a path-scoped rule so it loads only when Claude works with matching files.

2. AGENTS.md

Read the spec for this file before the blog posts about it. AGENTS.md cannot assign subdirectories to subagents, grant tool permissions, or sandbox anything.

The standard describes itself as "a simple, open format for guiding coding agents" and "a README for agents," and its own example file contains setup commands, code style, and test instructions.

Its value is portability. Over 60,000 open source projects use it, and it is read by Codex, Cursor, Gemini CLI, Copilot, Windsurf, Zed, Aider, Devin, Jules and others. Claude Code reads it as of v2.1.277. By default it reads AGENTS.md only when no CLAUDE.md or CLAUDE.local.md exists in the working directory or above it, and it reads both when you set Project instructions to claude-md-and-agents-md in /config.

3. The tri-layer spec

PRODUCT.md, DESIGN.md and BRAND.md carry the domain constraints. PRODUCT.md holds what the product does, DESIGN.md what the interface is allowed to look like, and BRAND.md how it is allowed to sound. Splitting them matters because they have different readers and change at different speeds.

They are still context, so a DESIGN.md full of tokens will not stop an inline style from shipping, while a linter will.

Hooks enforce a rule on every run

The hooks guide says hooks run at specific points in the lifecycle, which "gives you deterministic control: certain actions always happen rather than relying on the LLM to choose to run them."

After the Voice section failed to hold, I added a UserPromptSubmit hook that injects the writing rules whenever a copy signal appears in the prompt. I also extended a pre-commit slop check to cover src/lib/brand.ts, the file holding my site's identity copy, which had been shipping with no gate over it at all, and the tree below shows every file that changed across the two repos.

code
jeffkliu.com/
├── .husky/pre-commit                  # runs the slop check on staged copy
└── src/lib/brand.ts                   # identity copy, now covered by the check
claude-brain/
├── hooks/userpromptsubmit/
│   └── deliver-writer-gate.py         # injects the writing rules on a copy signal
└── marketplace/plugins/jeff-skills/skills/anti-ai-slop/scripts/
    └── check_slop.py                  # the checker, with the proper-term fix

The pre-commit hook is short enough to read whole. It passes every staged copy file, including brand.ts, to the checker, and a tell the commit adds blocks the commit.

.husky/pre-commit

sh
# Slop gate on staged copy (STU-362). Runs locally, so catching careless copy costs no CI
# minutes. Checks the staged version of MDX (frontmatter included), content.ts/entries.ts, and
# src/lib/brand.ts (the identity copy — added 2026-09-20 after it shipped AI-slop with no gate)
# against HEAD: a tell the commit adds or worsens blocks, rhythm included. Copy already there
# does not block (STU-363 sweeps it). The checker lives in claude-brain (~/.claude).

gate="$HOME/.claude/scripts/slop-staged.py"
if [ ! -f "$gate" ]; then
  echo "pre-commit: $gate not found, so staged copy cannot be checked. Pull claude-brain main (it ships in claude-brain#100) and commit again." >&2
  exit 1
fi

git diff --cached --name-only --diff-filter=ACMR -z \
  | grep -z -E '^content/.*\.mdx?$|(^|/)(content|entries)\.ts$|^src/lib/brand\.ts$' \
  | xargs -0 -r python3 "$gate"

On the next commit, the gate blocked a draft the model had padded with tells. It then threw a false positive. It read "Core Web Vitals" as the filler word "vital" and blocked a term I needed. I fixed the checker instead of the sentence. check_slop.py now matches "Core Web Vitals" as a proper term before it counts filler words, and I re-ran it against real filler to confirm it still fired.

Why not just put it all in JSON?

JSON is structured, machine-readable and unambiguous, so pointing CLAUDE.md at a config file with @context.json ought to be stricter than a page of prose the model can skim.

The memory page covers this case. Imports are "expanded and loaded into context at launch," which means an imported JSON file lands in the same context window as the prose around it, and Claude treats it as context like the rest. Braces, quotes, and repeated keys add tokens that prose does not need, on a page that warns longer files "consume more context and reduce adherence."

I ran into this directly when I asked a model whether importing JSON into CLAUDE.md would be more deterministic, and got a confident yes with three citations behind it. The first pointed at a real Vercel project, json-render, described as a standard for parsing configuration, when it is a generative UI framework that renders components from a fixed catalog at runtime. The second named an open context.json specification I could not find at any obvious address, and a repository search returned nothing. The third took shadcn's components.json, a file that configures a CLI, and promoted it to an enforcement layer, and one documentation page settled all three of them in about a minute.

There is a real place for JSON, and it is wherever something executes the file. settings.json is deterministic because Claude Code's hook runner executes it. A JSON Schema check in a pre-commit hook is deterministic for the same reason. It validates the shape of your config file, and it does not check the behavior of the model reading it.

So the question to ask about any file is who reads it. A file the model reads should be short and written in prose, a file a program reads should be in whatever format that program parses best so it can fail the build, and a file both of them read belongs wherever the program can enforce it, with nothing left in CLAUDE.md but a pointer. That last shape is what a design token file read by a linter already is.

How to set it up

Put the five files in your repository root and write them for a reader who forgets everything between sessions. Then, for each rule you actually care about, work out what happens if the model ignores it. If the answer is nothing much, a markdown file is fine, and if the answer is that you ship something broken, write a hook, a linter, or a test.

Frequently asked

Does CLAUDE.md actually enforce anything?
No. Anthropic's memory documentation says Claude treats these files as context, not enforced configuration, and directs you to a PreToolUse hook to block an action regardless of what the model decides. The same page says longer files consume more context and reduce adherence.
What is the difference between CLAUDE.md and AGENTS.md?
Which tools read them. Both are project instructions and neither enforces anything. AGENTS.md is an open format read by many tools including Codex, Cursor, Gemini CLI, Copilot, Windsurf and Zed, while CLAUDE.md is specific to Claude Code. As of v2.1.277, Claude Code reads AGENTS.md when a repository has no CLAUDE.md, and reads both when Project instructions is set to claude-md-and-agents-md.
Can AGENTS.md restrict which directories an agent is allowed to edit?
No. The standard describes itself as a README for agents, and its example file contains setup commands, code style and test instructions. Permissions and sandboxing come from the harness, through hooks and settings.
When should a rule become a hook instead of a line in CLAUDE.md?
Ask what happens if the model ignores it. If the cost is small, prose is fine and cheaper. If the cost is shipping something broken, it needs a mechanism that runs every time, which means a hook, a linter, or a test.
How long should CLAUDE.md be?
Under 200 lines, which is the target in Anthropic's memory documentation. The file loads into context at the start of every session, and longer files consume more context and reduce adherence. Move multi-step procedures into a skill and file-specific guidance into a path-scoped rule.
Share

Get in touch

Working end to end across design, creative, and engineering, where creativity, collaboration, and ownership build toward the company's goals.

jeffkliu@gmail.com