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

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:
| Scope | Location | Loads when |
|---|---|---|
| Managed policy | /Library/Application Support/ClaudeCode/CLAUDE.md on macOS | Every session, cannot be excluded |
| User | ~/.claude/CLAUDE.md and ~/.claude/rules/*.md | Every session, every project |
| Project | ./CLAUDE.md or ./.claude/CLAUDE.md | Every session in that project |
| Project rules | .claude/rules/*.md | Depends entirely on frontmatter |
| Local | ./CLAUDE.local.md | Every 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:
| Bucket | Files | Bytes | Approx tokens | Cost |
|---|---|---|---|---|
| Unconditional | 22 | 35,715 | ~8,900 | Every session, every project |
| Path-scoped | 55 | 86,570 | ~21,600 | Only 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:
| Pattern | Matches |
|---|---|
**/*.ts | TypeScript files in any directory |
src/**/* | Everything under src/ |
*.md | Markdown 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/filesyntax 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
PreToolUsehook or inpermissions.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.


