Tutorial1 day ago

Our Claude Code Setup Was Loading Every Rule Twice in Two Languages

An audit of 77 Claude Code rule files found 22 loading on every session, and 10 of those were Chinese translations of the other 10. That is 4,265 tokens of duplicate instruction per session.

The WJS Desk

Sep 1, 2026 · updated 4 hours ago · 8 min read

Photo by Element5 Digital on Pexels

We audited the Claude Code rule files on our machine expecting to trim a bit of fat. What we found was that 10 of the 22 files loading into every single session were Chinese translations of the other 10. The same coding standards, the same test coverage rule, the same git workflow, stated twice, in two languages, before any of us typed a word.

That is 4,265 tokens of duplicate instruction per session. Not catastrophic on its own. But it was invisible, it had been there for months, and finding it took one find command. Here is the audit, the fix, and the mechanism that makes rules cheap when you use it and expensive when you do not.

What you will end up with

A repeatable audit of every instruction file Claude Code loads on your machine, split into two buckets: the ones that load unconditionally on every session in every project, and the ones that only load when Claude touches a matching file. Plus the frontmatter change that moves files from the first bucket to the second.

It takes about 15 minutes. You need Claude Code installed and a shell. The commands are read-only until the last section, so you can run the audit on a machine you are nervous about.

Where instructions actually come from

Claude Code assembles its persistent instructions from more places than most people have in their head. In load order, broadest to most specific:

ScopeLocationLoads when
Managed policy/Library/Application Support/ClaudeCode/CLAUDE.md on macOSEvery session, cannot be excluded
User~/.claude/CLAUDE.md and ~/.claude/rules/*.mdEvery session, every project
Project./CLAUDE.md or ./.claude/CLAUDE.mdEvery session in that project
Project rules.claude/rules/*.mdDepends entirely on frontmatter
Local./CLAUDE.local.mdEvery session, gitignored

Two details from this list caused our problem. Rules are discovered recursively, so a subdirectory you created for organisation is still loaded. And a rule file with no paths frontmatter loads unconditionally, at the same priority as your project CLAUDE.md.

Organising rules into folders feels like it scopes them. It does not. The folder is decoration; the frontmatter is the mechanism.

The audit

Every rule file either has paths: frontmatter or it does not. That single bit decides whether it costs you tokens on every session or only when relevant. So sort by it:

cd ~/.claude/rules
for f in $(find . -name '*.md' | sort); do
  if head -5 "$f" | grep -q '^paths:'; then
    echo "SCOPED $f"
  else
    echo "ALWAYS $f"
  fi
done

Then measure the two buckets. Bytes divided by four is a rough token estimate, good enough to make a decision with:

find . -name '*.md' | while read f; do
  head -5 "$f" | grep -q '^paths:' || echo "$f"
done | xargs cat | wc -c

Our numbers:

BucketFilesBytesApprox tokensCost
Unconditional2235,715~8,900Every session, every project
Path-scoped5586,570~21,600Only on matching files

The good news first: 55 of 77 files were doing the right thing. The path-scoped bucket is more than twice the size of the unconditional one, and none of it loads until Claude opens a matching file. Whoever set this up understood the mechanism.

The problem was entirely in the other 22.

What we found in the always-on bucket

Listing it made the duplication obvious immediately:

./common/agents.md                ./zh/agents.md
./common/code-review.md           ./zh/code-review.md
./common/coding-style.md          ./zh/coding-style.md
./common/development-workflow.md  ./zh/development-workflow.md
./common/git-workflow.md          ./zh/git-workflow.md
./common/hooks.md                 ./zh/hooks.md
./common/patterns.md              ./zh/patterns.md
./common/performance.md           ./zh/performance.md
./common/security.md              ./zh/security.md
./common/testing.md               ./zh/testing.md
./README.md                       ./zh/README.md

Every single file in common/ had a twin in zh/. The zh/ directory is a Chinese translation of the English rules, added so the ruleset could be shared with Chinese-speaking developers. Entirely reasonable as a repository. Completely wrong as an installed configuration, because installing both means both load.

common/: 14,343 bytes (~3,585 tokens)
zh/    : 17,062 bytes (~4,265 tokens)

The translation is larger than the original. So the duplicate copy was the more expensive one.

The real cost is not the tokens. It is that the documentation is explicit about what happens when instructions conflict: Claude may pick one arbitrarily. Two independently maintained copies of the same rules will drift. When the English says one thing and the translation says another, you have introduced non-determinism into your own configuration and you will not know which copy won.

We also found a third README in the set. README.md at the rules root is documentation about the ruleset: how to install it, how to add a language, the directory layout. Useful to a human reading the repo. Loaded into context on every session, it is 4,310 bytes explaining an installation procedure to a model that is not going to run it.

The fix, which is two lines of frontmatter

A rule becomes conditional by declaring which files it applies to. That is the whole mechanism:

---
paths:
  - "**/*.ts"
  - "**/*.tsx"
---

# Testing Requirements

## Minimum Test Coverage: 80%

That file now loads when Claude reads a TypeScript file and stays out of context otherwise. The glob patterns are ordinary:

PatternMatches
**/*.tsTypeScript files in any directory
src/**/*Everything under src/
*.mdMarkdown in the project root only
src/**/*.{ts,tsx}Brace expansion, both extensions

For our audit the fix was not frontmatter at all, it was deletion. A translated duplicate does not need scoping, it needs to not be installed:

rm -rf ~/.claude/rules/zh
rm ~/.claude/rules/README.md

That is roughly 5,300 tokens back on every session, with zero loss of instruction, because every rule in it still exists in English.

What broke

Two things, both from assumptions we had not checked.

Nesting does not scope. We assumed rules/rust/ only applied to Rust projects because it was in a folder called rust. It does not work that way. Those files are scoped because someone put paths: ["**/*.rs"] in them, and if that frontmatter were missing, every Rust rule would load in a Python project. We confirmed this by checking the actual files rather than trusting the layout:

head -6 ~/.claude/rules/rust/security.md
---
paths:
  - "**/*.rs"
---
# Rust Security

Correct, but by intent and not by folder.

Brace expansion has a budget, and it fails quietly. Each brace group multiplies the pattern count, so {a,b}/{c,d}/*.{ts,tsx} is eight patterns from one line. A rule's whole paths list shares a budget of 1,000 expanded patterns. Exceed it and Claude Code uses the pattern unexpanded, at which point the literal braces match no files and your rule silently applies to nothing. There is no error. The rule is just never there.

The same class of quiet failure applies to square brackets. Glob syntax reads [ as the start of a character class, so a pattern like photos [2024/** is invalid and matches nothing. Escape the bracket to match it literally.

How to verify what actually loaded

Do not infer this from the filesystem. Ask the session. Inside Claude Code, /context lists the files that made it in under Memory files. If a file you expected is not in that list, it did not load, and no amount of correct-looking configuration changes that.

/memory is the companion command: it lists your CLAUDE.md and CLAUDE.local.md locations across user and project scope and opens any of them in your editor. Between the two, /memory tells you what exists and /context tells you what loaded. The gap between those two answers is where the bugs live.

Pro tip: Block-level HTML comments in a CLAUDE.md are stripped before the content reaches the model, so a maintainer note wrapped in comment markers costs zero tokens while staying visible to humans reading the file. It is the right place for notes explaining why a rule exists.

Common mistakes

  • Assuming imports save context. The @path/to/file syntax is for organisation only. Imported files are expanded and loaded at launch, so splitting a 400-line CLAUDE.md into four files costs exactly the same as leaving it. Path-scoped rules are the mechanism that actually reduces load, not imports.
  • Installing a whole rules repository. Ours shipped nine language variants and a translation. Copying the lot means the always-on files from all of them load together. Install the directories you use.
  • Putting a directory tree in CLAUDE.md. Claude can derive the layout by looking. Spend the tokens on the conventions it cannot derive, like why one module does not follow the pattern.
  • Writing rules Claude cannot verify. "Format code properly" is unfalsifiable. "Use 2-space indentation" is checkable, and checkable instructions are followed more consistently.
  • Expecting CLAUDE.md to enforce anything. It is delivered as context, not configuration. If a rule must hold regardless of what the model decides, it belongs in a PreToolUse hook or in permissions.deny, not in prose.
  • Leaving stale rules in place. A rule describing a build system you migrated off is not neutral. It is an instruction to do the wrong thing, sitting at the same priority as the correct ones.

What we would not do yet

We are not going to path-scope everything. A rule saying "never commit secrets" should load unconditionally, because the moment it depends on a file glob is the moment it misses the file that mattered. The audit is about finding rules that are accidentally unconditional, not about driving the always-on bucket to zero.

We are also leaving the 55 path-scoped files alone despite them being 21,600 tokens on paper. They cost nothing until a matching file is read, and when a Rust file is open, Rust rules are exactly what should be in context. Size is not the metric. Relevance-weighted size is.

The rollback

Every change here is a file operation, so take a copy of the directory before you touch it:

cp -R ~/.claude/rules ~/.claude/rules.backup

Putting the directory under version control works just as well and gives you a diff. Adding paths frontmatter is non-destructive and instantly reversible: delete the four lines and the rule goes back to loading always. Deleting a rules directory is not, which is why the backup goes first.

The thing we took away is that a configuration you have never audited is not the configuration you think you have. Ours had been quietly telling the model everything twice, in two languages, and it took one find loop to see it.

Share

We audited our Claude Code rules: 22 files load on every session, and 10 of them were translations of the other 10. Same advice, twice, in two languages. #ClaudeCode #DevTools #AI

Never miss a ship

The best stuff that shipped this week, delivered every Thursday. Free, no spam. We read all the boring stuff so you get the fun parts.

Keep reading