I had written design rules for my coding agent: UI guidelines, React patterns, how to structure Storybook stories, how to write interface copy. They sit in files that apply to the frontend folder, and I expected the agent to pick them up whenever it touched a component. Then I'd review the output and find it didn't match them. My first explanation was the usual one: the agent read the rules and drifted. I was wrong, at least in my transcripts. In many of the sessions where a rule should have applied, it was never in the agent's context at all.
Two things cause that. Claude Code loads a folder-scoped rule only when its own file tools open or change a matching file. And Claude Code itself, in its two most autonomous modes, tells the agent to prefer shell commands over those tools.
My agent wasn't ignoring the rules; Claude Code never gave them to it. Claude Code hooks let you run a script before a tool call, so I added a small one that runs before every shell command and every file write and sends work on project files to the dedicated tools. Rule loading went from about half the time to almost always.
Why your design rules may never load
A rule in .claude/rules/ can be set to apply to one folder by listing that folder in its paths: section (rules scoping). Such a rule loads only when Claude Code's own file tools touch a matching file: Read when it opens one, and Edit or Write when they change one (since version 2.1.288; before that, only Read did). Open the same file with cat, or change it with sed, and the rule stays on disk (shell commands don't load rules).
My UI guidelines and my React and Storybook patterns all work this way. I scoped them so the agent sees them when it opens a component and doesn't carry them around while it edits a backend script or a config file.
The transcript looks normal: the agent edits a component, the rule for that folder never appears, and nothing in the session warns you. From the output alone you can't tell a rule the agent ignored from one it never received.
Claude Code itself pushes the agent onto that path. In its two most autonomous modes, it tells the agent to read files with shell commands like cat and sed, and to make changes with sed or heredocs (a way to write a whole file from inside one shell command), "rather than using the dedicated Read, Edit, or Write tools" (the instruction). Anthropic's earlier guidance said the opposite: use the dedicated tools, not their shell equivalents (earlier instruction, release note).
Claude Code added the new instruction in August 2026. People have reported it several times (binary report, auto-mode report, bypass-mode report), and I found no reply from Anthropic on any of them.
Before changing anything, I checked whether this was happening in my own sessions. I analyzed about a week of my transcripts, main sessions and subagents. Claude Code keeps these session logs in ~/.claude/projects; subagents are helper agents it starts for side tasks. I counted how the agent touched code files, and how often each rule loaded when a session touched a file it covers.
- Claude Code's own tools handled 34% of file operations on code (3,956 of 11,731).
- A rule loaded 53% of the times a session touched a file it covers.
- The design rules alone loaded 51% of the time (572 of 1,130). In 551 of those cases the agent touched the frontend files only through the shell, and the rule loaded in 6.
Those last numbers settled it: when the agent reached the frontend only through the shell, the design rules almost never loaded. (They come from my own sessions, so they show the mechanism and the rough size of the gap, not a benchmark of Claude Code in general.)
Weighing the alternatives
Steering the agent to Read and Edit meant giving up two code tools I had set up, so I benchmarked them before dropping them. Both were ways to give the agent a language server, the code-intelligence service behind your editor's go-to-definition and rename: one through Claude Code's built-in setup, the other through Serena, an MCP server. With one, the agent can find a function by name or rename it everywhere it's used, instead of searching through text. If either one carried its weight, I would have kept it and loaded the rules myself some other way.
On 12 small tasks, accuracy was equal across every configuration. That alone didn't decide it, so I audited the runs to see what the tools were actually doing. Serena never used its symbol lookup, and its rename touched only the definition. The built-in setup never called the language server at all.
When I forced Serena to use only its name-based tools, it spent about three times the tokens on renames. Claude Code's built-in search matched shell grep and was a bit faster. My tasks were small, and on them neither tool earned its place, so I dropped both.
Before any of this, I had tried a large router that checked every shell command against a list of approved commands. It broke another tool's scripts, so I switched it off.
The hook
Mine leaves shell commands alone unless they touch the project's code or doc files. On those files it sends each shell operation to the matching tool: reads to Read, in-place edits to Edit, searches to Grep, file lookups to Glob. It also blocks a write over an existing file, so the change goes through Edit and shows up as a diff.
For UI review, that diff is what I want. I can see exactly which lines of a component changed, instead of reading a whole rewritten file and hunting for the difference. In my tests, the block on overwriting an existing file held even in bypass mode, the setting that skips Claude Code's permission prompts.
When the hook can't tell what a command does to a project file, it blocks the command rather than let it through. I'd rather the agent rephrase an unclear shell command than slip a rule-skipping read past the check.
When it blocks something, the hook names the tool to use instead, and Claude Code passes that reason to the agent (hooks). That message is the only thing the script puts into the agent's context: no rule text, no reminders competing with the design rules for attention.
Claude Code runs the hook as code rather than keeping it in context, so compaction doesn't touch it. Scoped rules don't get that treatment: after compaction they're gone until a matching file is read, edited or written again (context window).
Hooks register in a settings file. Mine is in ~/.claude/settings.json, matched on shell commands and file writes, with a 10-second limit:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash|Write", "hooks": [ { "type": "command", "command": "python3 ~/.claude/hooks/code-tool-guard/guard.py", "timeout": 10 } ] } ] }}The entry names only the tool, so the script itself checks which file a command touches.
There is one more piece, and I got the order wrong. On the native Mac and Linux builds, Claude Code hides Grep and Glob unless you launch it with a flag. Adding them to your settings file doesn't bring them back; only the launch flag does (details).
Without that flag, search runs as a shell command, which the hook refuses on project files. I made the flag permanent with a small wrapper placed ahead of the real claude on my PATH, the list of folders your terminal searches for commands:
#!/bin/sh# ~/.local/claude-wrapper/bin/claude, placed ahead of the real claude on PATHexec "$HOME/.local/bin/claude" --allowedTools=Grep,Glob "$@"I turned the hook on about 30 minutes before I had the wrapper in place, so for half an hour it kept pointing the agent to a search tool the agent didn't have. Turn on Grep and Glob first, confirm they show up, then enable the hook.
Results
With the hook on, all but 8 file operations on code went through Read, Edit, Write, Grep and Glob, and the design rules loaded almost every time their files were touched. The after column covers about three days of sessions, so it's a smaller sample than the before column:
| Measure | Before: about a week | After: about three days |
|---|---|---|
| Reads through Read | 54% | 99.9% |
| Searches through Grep | 0% | 100% |
| Edits and writes through Edit or Write | 78% | 100% |
| Code file operations through Read, Edit, Write, Grep and Glob | 34% (3,956 of 11,731) | 99.7% (2,780 of 2,788) |
| Design rules loaded when a session touched a file they cover | 51% (572 of 1,130) | 93% (84 of 90) |
| Shell attempts on project files blocked by the hook | none, no hook | 128 (of 148 blocks) |
Searches had the widest gap, because Grep was hidden during the before week and the agent had no dedicated search tool to use. Edits were already mostly going through Edit and Write. Reads were barely above half, and reads are the row to watch for the design rules: opening or changing a file with Claude Code's own tools, rather than with cat or sed, is what makes it load the rule for that folder. Routing searches to Grep and edits to Edit keeps the rest of the work visible and reviewable.
The 6 misses came from 3 subagents that had already opened the rule files directly, so the rule text was in context anyway. I checked their transcripts.
What the numbers measure is loading, not following. I can't tell from them whether the agent applies a rule once it has it. That's a different problem, for automated checks and review; loading only makes it possible.
What it costs and what's still open
I pay for this in tokens and in a handful of wrong blocks, and a few gaps are still open.
- Read and Edit cost about 25% more tokens than Serena did on edits. With Serena I would have had to load the rules myself, so I expect the real gap to be smaller. In exchange I get rules in context and component changes I can review line by line, and for UI work I'll take that trade.
- 18 of the hook's 148 blocks were mistakes. (The other 130 are the 128 shell operations in the table, plus a copy into the project from another worktree and a write over an existing file.) Each time, the agent was working with a temporary file, but the file path came from a shell variable the hook couldn't read, so the hook assumed the file was inside the project and blocked the command. Teaching the hook to read the variable fixed it, and there have been none since.
- 8 shell operations got past the hook. Each one read files in another worktree of the same repository, which the hook treats as outside the project.
- Once, an agent deleted a file and wrote it from scratch instead of editing it, which skips the diff the Write block is meant to force. I checked: it was a full rewrite, so starting over was reasonable, and I left that gap open.
- Outside the design rules, 11 rule misses are still unexplained, including one session that loaded no rules at all. I'm still investigating those.
There is also an undocumented setting that turns the shell-first instruction off. In my test it worked both ways: with the setting on, the instruction was in the agent's context; with it off, it was gone (details). Being undocumented, it could change in any release. The hook works whether or not that instruction is there, so I don't depend on the setting.
Try it yourself
Before adding a hook, check whether your sessions have the same gap. Paste this into Claude Code from your project:
Go through my Claude Code transcripts from the last week in ~/.claude/projects. List every shell command that read, searched or edited a code file in my project, grouped by command (cat, sed, grep, find...), with counts. Then list my rules in .claude/rules that have a paths: section, and for each one tell me how often a session touched a file it covers and how often the rule actually loaded.After a few days with the hook on, run this second prompt to compare:
Repeat the same analysis for the sessions since I turned on the hook, and compare: how many file operations on code went through Read, Edit, Write, Grep and Glob before and after, and how often each rule loaded when its files were touched. Show the blocks the hook made and flag any that look like mistakes.If the shell counts are high and the rule load rate is low, the minimum setup is short:
- Launch Claude Code with Grep and Glob enabled. On native Mac or Linux builds, use the wrapper above or pass the flag by hand, and confirm both tools appear before going further.
- Write the guard script at
~/.claude/hooks/code-tool-guard/guard.py. It should:- define which paths count as project code or doc files;
- catch shell reads and edits on those paths, along with searches and file lookups, then refuse them and name the tool to use instead;
- when a Write would land on an existing file, refuse it and point to Edit;
- refuse anything it can't classify.
- Register the hook in
~/.claude/settings.jsonwith the entry above, then open/hooksand check that it is listed. - Work normally for a few days, then run the second prompt above.
Read the flagged blocks first. Mine were all the same shape, a temporary-file path the hook misread. Fix that kind of gap in the script, then check another day of transcripts.