docs: add rules frontmatter paths: syntax examples and validation hook#26914
docs: add rules frontmatter paths: syntax examples and validation hook#26914Johntycour wants to merge 1 commit intoanthropics:mainfrom
Conversation
The `paths:` field in rules frontmatter silently fails when using YAML array syntax or JSON inline array syntax. This is because the internal CSV parser iterates the array elements instead of characters, producing concatenated invalid globs that match nothing. This PR adds: - examples/rules/README.md documenting correct vs broken syntax - examples/rules/typescript-style.md and python-style.md as working examples - examples/hooks/rules_frontmatter_validator.py to detect broken syntax Fixes are tracked in: - anthropics#19377 (YAML array syntax fails) - anthropics#13905 (paths: not triggering) - anthropics#21858 (rules not applying) - anthropics#17204 (quoted paths: broken)
There was a problem hiding this comment.
Pull request overview
Adds documentation and tooling examples to help users avoid silent failures when using paths: in Claude Code rules frontmatter by providing correct/incorrect examples and a hook-based validator.
Changes:
- Added example rules files demonstrating working CSV-string frontmatter usage (
paths:/globs:). - Added a PostToolUse Python hook that warns when rules frontmatter uses YAML-list or JSON-array syntax.
- Documented the root cause behind silent failures and linked related issues.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| examples/rules/typescript-style.md | Adds a TypeScript rules example using CSV-string paths: syntax. |
| examples/rules/python-style.md | Adds a Python rules example using CSV-string globs: syntax. |
| examples/rules/README.md | Documents correct vs broken frontmatter formats and explains the failure mode. |
| examples/hooks/rules_frontmatter_validator.py | Adds a hook script to detect broken frontmatter syntax after Write/Edit. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| def extract_frontmatter(content: str) -> str | None: | ||
| """Extract YAML frontmatter from a markdown file.""" | ||
| match = re.match(r"^---\s*\n(.*?)\n---", content, re.DOTALL) |
There was a problem hiding this comment.
extract_frontmatter only matches LF line endings (\n). On Windows/CRLF rules files, this won’t detect frontmatter and the validator will silently do nothing. Consider using \r?\n in the regex (and optionally allowing ---\s*\r?\n for both delimiters).
| match = re.match(r"^---\s*\n(.*?)\n---", content, re.DOTALL) | |
| match = re.match(r"^---\s*\r?\n(.*?)\r?\n---", content, re.DOTALL) |
| # Pattern 1: YAML list syntax (field: followed by newline then " - ") | ||
| if re.search(rf"^{field}:\s*$", frontmatter, re.MULTILINE) and re.search( | ||
| r"^\s+-\s+", frontmatter, re.MULTILINE | ||
| ): |
There was a problem hiding this comment.
The YAML-list detection can false-positive because it checks for paths: (or globs:) being present and then any list item anywhere in the frontmatter, even if the list belongs to a different key. Tighten the regex to ensure the - items are actually under the same field (e.g., match the block starting at ^{field}: and subsequent indented - lines).
| # Pattern 1: YAML list syntax (field: followed by newline then " - ") | |
| if re.search(rf"^{field}:\s*$", frontmatter, re.MULTILINE) and re.search( | |
| r"^\s+-\s+", frontmatter, re.MULTILINE | |
| ): | |
| # Pattern 1: YAML list syntax (field: followed by newline then indented "- " lines) | |
| yaml_list_pattern = rf"^{field}:\s*\n(?:[ \t]+-\s+[^\n]*\n?)+" | |
| if re.search(yaml_list_pattern, frontmatter, re.MULTILINE): |
| rules_indicators = ["/rules/", "/.claude/rules/"] | ||
| if not any(indicator in file_path for indicator in rules_indicators): |
There was a problem hiding this comment.
Two issues that will prevent this from working reliably on Windows paths: (1) rules_indicators only checks for forward-slash substrings, so C:\\...\\.claude\\rules\\x.md won’t match; (2) the docstring says it targets .claude/rules/ but the code also matches any /rules/ directory. Consider normalizing file_path to POSIX separators before substring checks and narrowing (or documenting) the intended directories.
| rules_indicators = ["/rules/", "/.claude/rules/"] | |
| if not any(indicator in file_path for indicator in rules_indicators): | |
| # Normalize to POSIX-style separators for reliable substring checks | |
| normalized_path = Path(file_path).as_posix() | |
| # Only process rules files in .claude/rules/ as documented | |
| rules_indicators = ["/.claude/rules/", ".claude/rules/"] | |
| if not any(indicator in normalized_path for indicator in rules_indicators): |
| print( | ||
| f"WARNING: Rules file {path.name} has frontmatter issues:\n" | ||
| + "\n".join(f" - {issue}" for issue in issues), | ||
| file=sys.stderr, |
There was a problem hiding this comment.
This prints the main warning to stderr but exits with code 0. Per the hooks docs, PostToolUse only reliably shows stdout on exit 0; stderr is typically only surfaced on exit 2. To ensure the warning is visible, print to stdout (or emit the standard JSON output with systemMessage) and keep exit 0.
| file=sys.stderr, |
|
|
||
| ```yaml | ||
| --- | ||
| paths: "**/*.ts" |
There was a problem hiding this comment.
The heading says “Single glob (unquoted)”, but the example value is quoted ("**/*.ts"). Either update the heading to say quoted, or change the example to be truly unquoted to avoid confusing readers about what’s required/allowed.
| paths: "**/*.ts" | |
| paths: **/*.ts |
| ### Quoted single value with paths: — MAY BREAK | ||
|
|
||
| ```yaml | ||
| --- | ||
| # In some versions, quoted paths: may also fail | ||
| # Use globs: if you need quoted values | ||
| paths: "**/*.cs" | ||
| --- | ||
| ``` | ||
|
|
||
| See [#17204](https://github.com/anthropics/claude-code/issues/17204) — `paths:` with quoted values may undergo additional processing that strips or misinterprets the value. Use `globs:` as a more reliable alternative. |
There was a problem hiding this comment.
This section labels quoted paths: as “MAY BREAK”, but earlier in “Correct Syntax” the recommended examples for paths: are also quoted. Please clarify the guidance (e.g., recommend unquoted paths: vs globs: consistently, and specify which versions/contexts the quoted form fails in).
|
|
||
| ## Validation Hook | ||
|
|
||
| See [`examples/hooks/rules_frontmatter_validator.py`](../hooks/rules_frontmatter_validator.py) — a PreToolUse hook that detects rules files using broken frontmatter syntax. |
There was a problem hiding this comment.
The README describes the validator as a “PreToolUse hook”, but the validator script and its sample config are PostToolUse. Please align the README with the intended hook event so users configure it correctly.
| See [`examples/hooks/rules_frontmatter_validator.py`](../hooks/rules_frontmatter_validator.py) — a PreToolUse hook that detects rules files using broken frontmatter syntax. | |
| See [`examples/hooks/rules_frontmatter_validator.py`](../hooks/rules_frontmatter_validator.py) — a PostToolUse hook that detects rules files using broken frontmatter syntax. |
Summary
examples/rules/directory with correct and incorrect frontmatter syntax examplesexamples/hooks/rules_frontmatter_validator.py— a PostToolUse hook that detects brokenpaths:syntaxRoot Cause
The
paths:field in rules frontmatter is parsed by an internal CSV parser (_9A()) that expects a string and iterates character by character. When YAML array syntax is used:The YAML parser returns a JavaScript
Array, which_9A()then iterates element by element instead of character by character. This concatenates all globs without any separator (e.g.,**/*.ts**/*.tsx), producing an invalid glob that matches nothing.Proposed one-line fix (in
_9A()caller)This normalizes the input before it reaches the CSV parser.
What's included
examples/rules/README.mdexamples/rules/typescript-style.mdpaths: "**/*.ts,**/*.tsx"examples/rules/python-style.mdglobs: "**/*.py,**/*.pyi"examples/hooks/rules_frontmatter_validator.pyRelated Issues
Closes #19377 (documentation aspect)
Related: #13905, #21858, #17204
Test plan
rules_frontmatter_validator.pydetects YAML list syntax in frontmatterrules_frontmatter_validator.pydetects JSON array syntax in frontmatterrules_frontmatter_validator.pyignores correct CSV syntax🤖 Generated with Claude Code