Documentation: .claude/rules/ frontmatter format incorrect - globs works, paths with quotes/YAML list does not

Status Closed — not planned
Maintainer reply None cached
Activity 11 comments · opened Jan 9, 2026 · closed May 16, 2026

Summary

The documentation for .claude/rules/ frontmatter is incorrect or incomplete. After extensive testing, the documented paths: format does not work in several configurations, while the undocumented globs: format works reliably.

Environment

  • Claude Code CLI
  • WSL2 (Linux on Windows)
  • Project with .claude/rules/ directory containing multiple rule files

Testing Methodology

Created multiple test files with different frontmatter formats and checked /memory output:

| Format | Loads? |
|--------|--------|
| No frontmatter (unconditional) | YES |
| globs: "**/*.cs" | YES |
| paths: **/*.cs (unquoted) | YES |
| paths: "**/*.cs" (quoted) | NO |
| paths: + YAML list | NO |

Expected Behavior

Based on documentation and GitHub issues, this should work:

---
paths:
  - "**/*.cs"
  - "**/Controllers/**"
---

Actual Behavior

Only these formats work:

---
globs: **/*.cs, **/Controllers/**
---

Impact

Users following the documentation will have non-functional path-scoped rules with no error messages indicating why rules are not loading. This is a silent failure that is very difficult to debug.

Suggested Fix

  1. Update documentation to show the working format (globs:)
  2. Or fix the parser to support the documented paths: formats

Workaround

Use globs: with comma-separated unquoted patterns:

---
globs: pattern1, pattern2, pattern3
---

View original on GitHub ↗

11 Comments

github-actions[bot] · 7 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/16038
  2. https://github.com/anthropics/claude-code/issues/13905
  3. https://github.com/anthropics/claude-code/issues/16853

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

tazbytes2019 · 7 months ago

Tried workaround using globs: with comma-separated unquoted patterns:
Unfortunately all rules files .claude/rules/ are loaded into the system prompt at conversation start, regardless of whether the user is working with files matching those patterns.
Claude CLI in windows env

amondnet · 7 months ago

https://github.com/amondnet/claude-memory-test?tab=readme-ov-file#test-results

| File | Format | Example | Loaded | |------|--------|---------|--------| | api-paths-quoted-csv.md | Quoted CSV | paths: "src/api/**/*,src/services/**/*" | ✅ | | api-paths-unquoted-csv.md | Unquoted CSV | paths: src/api/**/*,src/services/**/* | ✅ | | api-paths-yaml-array.md | YAML Array | paths:<br> - "src/api/**/*" | ❌ |
Johntycour · 6 months ago

Root cause analysis confirms your findings

I've traced the CC binary's rule loading pipeline and can confirm the behavior you documented.

Root Cause (detailed in #19377)

The paths: field is processed by _9A(), a CSV parser that iterates character by character. Three failure modes:

  1. YAML Array_9A() receives a JS Array, iterates elements not characters → broken concatenation
  2. JSON inline array ["a", "b"] → same issue
  3. Quoted single value → your finding that paths: "**/*.cs" fails while paths: **/*.cs works suggests an additional quoting issue specific to paths: vs globs:

Why globs: works but paths: doesn't with quotes

Both fields go through _9A(), but the paths: field likely has additional processing (path resolution in zKL()) that doesn't handle quoted strings properly. The quotes may be preserved in the path instead of being stripped, causing the glob to literally include " characters.

Proposed Fix

// In Puf(), before calling _9A():
const paths = _9A(Array.isArray(A.paths) ? A.paths.join(",") : String(A.paths));

Plus defensive handling in _9A() itself for non-string inputs.

See full analysis at: #19377 (comment)

maxjeltes · 5 months ago

Confirmed working format for path-scoped lazy loading (v2.2.x, VSCode extension)

Did some systematic testing using an InstructionsLoaded hook to audit which rule files load and when. Here's what I found:

What does NOT work

| Format | Result |
|--------|--------|
| paths: as YAML array with quoted strings | Rules either load eagerly at session start or not at all |
| paths: as YAML array without quotes | Same — does not lazy-load |
| globs: (comma-separated, unquoted) | Always loads eagerly at session start, even with alwaysApply: false |
| Simple directory paths (e.g. paths: app/javascript/) | Does not load at all |

What WORKS

---
alwaysApply: false
paths: app/javascript/**/*.js, app/javascript/**/*.ts, spec/javascript/**/*
description: "..."
---

Key requirements:

  • Use paths: (not globs:) as a single unquoted CSV line — not a YAML array, not quoted
  • Include alwaysApply: false
  • Both are required — paths: as CSV alone (without alwaysApply: false) still loaded eagerly

Audit log proof

Session start — only always-on rules load:

15:34:04 | Type: Project | Reason: session_start | File: .claude/rules/concerns.md
15:34:04 | Type: Project | Reason: session_start | File: .claude/rules/git-workflow.md
15:34:04 | Type: Project | Reason: include | File: AGENTS.md

After reading a matching .js file — path-scoped rule lazy-loads:

15:49:18 | Type: Project | Reason: path_glob_match | File: .claude/rules/frontend.md | Triggered by: app/javascript/debugtimeline/timeline_box.js | Globs: ["app/javascript/**/*.js", ...]

Audit hook setup

For anyone wanting to reproduce, add this to your ~/.claude/settings.json:

"hooks": {
  "InstructionsLoaded": [
    {
      "hooks": [
        {
          "type": "command",
          "command": "bash ~/.claude/hooks/log-instructions.sh"
        }
      ]
    }
  ]
}

With ~/.claude/hooks/log-instructions.sh:

#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.file_path // "N/A"')
MEMORY_TYPE=$(echo "$INPUT" | jq -r '.memory_type // "N/A"')
LOAD_REASON=$(echo "$INPUT" | jq -r '.load_reason // "N/A"')
TRIGGER_FILE=$(echo "$INPUT" | jq -r '.trigger_file_path // "N/A"')
GLOBS=$(echo "$INPUT" | jq -r '.globs // "N/A"')
TIMESTAMP=$(date +"%H:%M:%S")
LOG_ENTRY="$TIMESTAMP | Type: $MEMORY_TYPE | Reason: $LOAD_REASON | File: $FILE_PATH"
[[ "$TRIGGER_FILE" != "N/A" && "$TRIGGER_FILE" != "null" ]] && LOG_ENTRY="$LOG_ENTRY | Triggered by: $TRIGGER_FILE"
[[ "$GLOBS" != "N/A" && "$GLOBS" != "null" ]] && LOG_ENTRY="$LOG_ENTRY | Globs: $GLOBS"
echo "$LOG_ENTRY" >> ~/.claude/instruction-load-audit.log
exit 0

Then tail -f ~/.claude/instruction-load-audit.log in a separate terminal to watch in real-time.

luiseiman · 5 months ago

Workaround that works reliably: use globs: instead of paths:

---
globs: "**/*.ts,**/*.tsx"
---

The globs: field uses a different code path that handles comma-separated patterns correctly. It works with quoted strings, multiple patterns, and across all contexts (project rules, user rules, worktrees).

We maintain claude-kit, a Claude Code configuration factory used across ~10 projects. We switched all 30+ rule files to globs: after hitting the same silent failures with paths:. Zero issues since.

Hope this helps others land here from search. Would be great to see globs: documented officially or paths: fixed to use the same parser.

dsent · 5 months ago

@luiseiman

Workaround that works reliably: use globs: instead of paths: ``yaml --- globs: "**/*.ts,**/*.tsx" --- ``

The report above says:

globs: Always loads eagerly at session start, even with alwaysApply: false

Don't you experience the same?

I tried some time ago and found out that the instructions are loaded into context regardless of what files Claude is working on. Claude is still usually able to apply them discriminately, but the context gets bloated so you can just put everything into CLAUDE.md with the same effect.

luiseiman · 5 months ago

@dsent Good catch — yes, globs: alone loads eagerly. That's intentional on our side.

In dotforge we use globs: for eager loading because our rules are lightweight (< 50 lines each) and the context overhead is acceptable. We're not trying to lazy-load — we want glob-scoped rules that load at session start.

For lazy loading (rule loads only when Claude touches a matching file), @maxjeltes nailed the working combination:

---
alwaysApply: false
paths: src/**/*.ts, lib/**/*.ts
---

Key constraints:

  • paths: must be unquoted CSV on a single line — no YAML arrays, no quoted strings (parser bug confirmed by @Johntycour in #19377)
  • alwaysApply: false is required — without it, even paths: loads eagerly

So the two working patterns are:

| Goal | Frontmatter |
|------|------------|
| Eager (always in context) | globs: **/*.ts, **/*.tsx |
| Lazy (on file match only) | alwaysApply: false + paths: **/*.ts, **/*.tsx |

globs: without alwaysApply: false ≠ lazy. It filters but doesn't defer. The context bloat you're seeing is expected behavior for that format.

github-actions[bot] · 3 months ago

Closing for now — inactive for too long. Please open a new issue if this is still relevant.

cytoph · 2 months ago

Why was this closed? This is still an existing issue. And only because people don't re-explain it every time it still is. The syntax we now have to use (paths: **/*.ts, **/*.js) is not compliant with how front matter should look, it's not even legit YAML as far as I'm aware. And worst of all, the documentation states something that is just not working.

AgainPsychoX · 1 month ago

I hate auto-closing issues. I hate Claude day by day more and more, I feel betrayed and sad about it.