DEV Community

Rulestack
Rulestack

Posted on

Did Claude Code actually load your rules? /context, lazy loading, and the InstructionsLoaded hook

You wrote the rule. The agent ignored it. Before you rewrite the wording — check whether the rule was ever in the context window at all. In my experience most "Claude ignored my CLAUDE.md" reports are actually loading problems, and loading problems are checkable in seconds.

Here are the three checks I run, in order.

1. /context — the ground truth for the current session

Run /context in your session and look at the Memory files list. That list is what actually loaded — not what exists on disk, not what should have loaded. If your file isn't there, no amount of prompt-wording work will help.

Don't confuse it with /memory. The /memory command lists memory file locations across user and project scopes — including entries for files that don't exist yet, so you can create them. It answers "where could instructions live?" /context answers "what is Claude actually reading right now?" When you're debugging, only the second question matters.

2. Know the two lazy-loading behaviors

Two loading rules produce almost all of the "my rule vanished" confusion:

Nested CLAUDE.md files load on demand. CLAUDE.md files in the directory hierarchy above your working directory load in full at launch. But a CLAUDE.md sitting in a subdirectory below it does not load until Claude actually reads a file in that subtree. Early in a session, that rule effectively does not exist — and /context will honestly show it missing until the first file access triggers it. If a rule must always apply, keep it in the root file (or a .claude/rules/ file without a path scope), not in a nested CLAUDE.md.

Imports have sharp edges. @path/to/file imports load at launch alongside the file that references them, and they can chain — but only to a maximum depth of four hops. Two details bite people:

  • Import parsing skips code spans and fenced code blocks. `@README` in backticks is literal text; @README outside backticks is an import. If you documented an import inside a code fence while "cleaning up", you silently disabled it.
  • Relative paths resolve relative to the file containing the import, not your working directory. A fragment that imports ./shared.md breaks when you move it.

In practice I've never needed more than two hops on a real project (root importing AGENTS.md, which sometimes imports a shared fragment). If you're approaching four, the chain itself is usually the thing to fix.

3. InstructionsLoaded — log every load as it happens

Since you can't sit in a session running /context after every file access, Claude Code has a hook event for exactly this: InstructionsLoaded fires every time a CLAUDE.md or .claude/rules/*.md file enters context — at session start for eagerly-loaded files, and again mid-session when a nested file or path-scoped rule lazily loads.

The hook input tells you three things: file_path (which file), memory_type (User / Project / Local / Managed), and load_reason. The load_reason values are the interesting part:

  • session_start — loaded eagerly at launch
  • nested_traversal — a subdirectory CLAUDE.md just lazy-loaded
  • path_glob_match — a path-scoped rule matched a file Claude touched
  • include — pulled in via an @path import
  • compact — reloaded after context compaction

A minimal logger in .claude/settings.json:

{
  "hooks": {
    "InstructionsLoaded": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '\"\\(.load_reason)\\t\\(.file_path)\"' >> ~/.claude/instructions-loaded.log"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Now tail -f ~/.claude/instructions-loaded.log shows you the exact moment that nested CLAUDE.md finally entered context — instead of you inferring it from a behavior change three prompts later. The hook is observability-only (it runs async, exit codes are ignored), so it can't break anything.

You can also filter with a matcher on the load reason, e.g. "matcher": "nested_traversal|path_glob_match" to log only lazy loads.

The 30-second debugging flow

  1. Rule ignored? Run /context. File not under Memory files → loading problem, stop editing the wording.
  2. File is nested in a subdirectory → expected: it loads only after Claude reads that subtree. Move it to the root if it must always apply.
  3. File is imported → check the import isn't inside backticks or a code fence, check the relative path from the importing file, and count your hops (max four).
  4. File loaded but behavior is still wrong → now it's an instruction-quality problem. Rewrite for specificity, check for contradicting rules, and keep the file under ~200 lines.

Only step 4 is a prompt-engineering problem. Steps 1–3 are mechanical, and the InstructionsLoaded log turns them from guesswork into a grep.

For the full resolution order (which files load, in what order, and what wins), I walked through it in Which CLAUDE.md Files Claude Code Actually Loads (and in What Order).


I maintain Rulestack — tested rules packs and skills for Claude Code, Cursor, and Codex, kept in sync with how these tools actually load configuration.

Daily notes on AI coding agents: @ai-shop.bsky.social

Top comments (2)

Collapse
 
alexshev profile image
Alex Shev

The /context distinction is important. A lot of agent debugging starts with rewriting rules that were never loaded. I like treating instruction loading as an observable system state: first verify the rule is present, then debug whether the model followed it.

Collapse
 
rulestack profile image
Rulestack

'Observable system state' is a good frame. The two failures need opposite fixes — a rule that never loaded is a config problem, a loaded rule the model ignored is a writing problem — and checking /context first tells you which rewrite you're actually doing. One wrinkle I'd add: presence at session start doesn't guarantee the text survived compaction later in a long session, so for load-bearing rules I re-verify late too. Have you seen that drift on your side?