Skip to main content

AI·8 min read·

Exploring Claude CLI Context and Compliance with My Standards Repository

A CLAUDE.md is a prompt, not a loader. What a standards repository can and cannot enforce, and where the real enforcement has to live.

The problem: AI tools that forget everything

Every new session starts from nothing. You explain your coding style again. You paste the same security requirements. You get suggestions that contradict last week’s suggestions, and the only thing keeping any of it consistent is your own memory.

So I built github.com/williamzujkowski/standards — an MIT-licensed collection of development standards written to be read by a model rather than a person — and wired it into my projects through a CLAUDE.md. This sits alongside progressive context loading and prompt engineering as one more attempt at the same problem: getting a model to behave consistently without re-explaining yourself.

It works, with a large asterisk that took me a while to accept and which is most of what this post is about.

the CLI, with house rules

The asterisk: CLAUDE.md is a prompt, not a router

I described this to myself for months as an “intelligent router” that detects what you’re working on and loads the relevant standards. That is a flattering description of what happens, and it is wrong.

CLAUDE.md is a file the model reads at the start of a session. That’s the whole mechanism. When I write:

@load [CS:python + TS:pytest + SEC:*]

nothing parses that. There is no matching engine, no context detection, no dispatch. It is a convention I have asked the model to follow, expressed in a syntax that looks like an API because I found that shape easier to think in. The model usually follows it. “Usually” is doing real work in that sentence.

My own repository is explicit about this — docs/core/CLAUDE.md carries the note that the @load directive and its semantic loading syntax are planned rather than implemented, with the current version relying on a loader script. I wrote the aspirational syntax first and the honest note second, which is a fair summary of how this kind of project goes.

The one genuine mechanism in Claude Code is @path/to/file, which imports a file’s contents. That is a real feature and it is less magical than it sounds: it is textual inclusion, and the included text costs tokens like any other.

Why this matters if you are copying the pattern. A convention the model follows most of the time is genuinely useful and is not a control. If something must happen every time — a licence header, a security review, a compliance tag — it cannot live in a prompt. It has to live somewhere that fails the build.

On the token argument

The pitch for this pattern is usually token savings: replace a wall of pasted standards with a short reference.

The short reference is short. But the standards it points at still get read into context when they are actually needed, and CODING_STANDARDS.md on its own is around 23KB. So the saving is not “5,000 tokens became 100.” The saving is in the standards you don’t load on a given task — you pull the Python and testing conventions for a Python change and leave the frontend ones alone.

That is a real benefit and a much more modest one than a percentage implies. I would be sceptical of any headline figure here, including one of mine: the number depends entirely on what you compare against, and the tempting baseline — “loading every standard at once” — is a thing nobody does.

What’s actually in the repo

The standards live as one document per domain under docs/standards/CODING_STANDARDS.md, SECURITY_STANDARDS.md, TESTING_STANDARDS.md, CLOUD_NATIVE_STANDARDS.md, COMPLIANCE_STANDARDS.md, MICROSERVICES_STANDARDS.md, DEVOPS_PLATFORM_STANDARDS.md, DATABASE_STANDARDS.md and a dozen more. Each is a flat list of rules under headed sections, deliberately written to be quoted from rather than read end to end.

The shape that works, after several rewrites that didn’t:

One rule per bullet, stated imperatively. “Parameterise every SQL query” is usable. “SQL queries should generally be parameterised where practical” gives the model room to decide it isn’t practical, which it will take.

Rationale on its own line, not woven into the rule. A model asked to apply a rule needs the rule; a human deciding whether to keep the rule needs the reason. Splitting them means the rule can be quoted without dragging a paragraph along.

No cross-references between documents. Every one I wrote — “see the security standards for auth patterns” — either got followed at the cost of a large chunk of context, or got ignored. Both outcomes are worse than repeating four lines in both files.

Sections sized to be quoted whole. If a heading covers more than can reasonably be pulled into a prompt, it needs splitting. This is the constraint that most changed how I write them.

The routing problem, honestly

The CLAUDE.md at the top is the part I got most wrong for longest.

The mechanism I wanted was conditional loading: detect a Python project, pull the Python and testing standards, leave the frontend ones alone. What I built was a convention that asks the model to do that, in a syntax that looks like it’s addressing a loader. Sometimes it follows it. There’s no dispatch underneath.

What genuinely works today is much less clever:

@path/to/file imports. This is a real Claude Code feature — textual inclusion of a file’s contents. It costs tokens like anything else in context, but it is deterministic, which the convention is not.

A short index rather than a router. A CLAUDE.md that lists which document covers what, in a dozen lines, lets the model go and read the right one. That is a table of contents doing an honest job, rather than a dispatcher doing a dishonest one.

Keep it short. Mine drifts toward bloat every time I add a rule for some edge case, and a long instruction file gets followed less reliably, not more — the rules start competing with each other and with the actual request.

NIST control tagging

The part I have found most durable is compliance tagging. Working against NIST 800-53r5 has come up repeatedly over the years, and having the control mapping live next to the code rather than in a spreadsheet is the difference between compliance being a document and being a property of the repository.

The repo ships scripts/setup-nist-hooks.sh, which installs a pre-commit hook that checks for control annotations:

git clone https://github.com/williamzujkowski/standards
./standards/scripts/setup-nist-hooks.sh

Tag the code where the control is actually implemented — in a comment next to the function that does the work, not in a header block at the top of the file. The value is that a reviewer reading the authentication path sees which control it satisfies, and notices when a change breaks that relationship.

The pre-commit lesson worth keeping

I set up that hook feeling quite pleased with myself, then found I could sail straight past it:

git commit --no-verify

My first instinct was to close the hole from inside the hook. You cannot. --no-verify skips the pre-commit hook entirely — the hook process never starts, so nothing it might do on the way out can matter. There is no exit code that runs when the code doesn’t run.

This is obvious in retrospect and it was not obvious to me at the time, and I suspect I am not alone, because “add a pre-commit hook” is the standard advice for enforcing almost anything.

Client-side hooks are a convenience for the person running them. They are not enforcement. Real enforcement is server-side, and there are three places to put it:

  • a pre-receive hook on the remote, which cannot be bypassed by the client
  • a required CI status check, so a PR cannot merge until validation passes
  • branch protection, so nobody pushes to the default branch directly

The pre-commit hook is still worth having. It gives you the fast feedback loop, catching problems in the two seconds after you type git commit rather than five minutes later in CI. Just don’t mistake the convenience for the control — which is the same mistake, in a different costume, as mistaking a prompt for a router.

What I would tell someone starting this

Write the standards as if a model will read them, because one will. Short declarative rules, one concept per heading, no cross-references that require holding two documents in your head at once. The documents that work well for a model turn out to be the ones that work well for a new colleague.

Keep the CLAUDE.md short. Mine drifts toward bloat every time I add a rule for an edge case, and a long instruction file is one the model follows less reliably, not more — the rules compete for attention with each other and with your actual request.

Put anything load-bearing where it fails the build. Everything else is a strong suggestion, and strong suggestions are fine as long as you know which category a given rule is in.

Expect to throw away the enforcement layer. I rewrote mine repeatedly, and the versions that survived were the ones that checked fewer things more reliably.

Where this pattern has gone since

The naming mistake in this post — calling a convention a mechanism — turned out to be a general one. It became part of the corpus in Ninety-Two Posts as a Test Corpus.

Written in July 2025, and the ground has moved. Claude Code now has first-class Skills (.claude/skills/), path-scoped rules, hooks, and subagents — which between them cover most of what the @load convention was reaching for, with actual dispatch behind them rather than a hopeful convention.

The standards repository itself has since refactored toward a skills-based layout. If you are building this today, start there rather than from the syntax in this post. The underlying idea holds up: give the model your standards once, in a form it can use, and stop re-explaining yourself. The specific mechanism I reached for has been replaced by better ones, which is the normal and desirable outcome for a workaround.