-
Notifications
You must be signed in to change notification settings - Fork 5.5k
docs: add rules frontmatter paths: syntax examples and validation hook #26914
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,119 @@ | ||||||||||||||||||
| #!/usr/bin/env python3 | ||||||||||||||||||
| """ | ||||||||||||||||||
| Claude Code Hook: Rules Frontmatter Validator | ||||||||||||||||||
| =============================================== | ||||||||||||||||||
| This hook validates that rules files (.md) in .claude/rules/ use the correct | ||||||||||||||||||
| CSV string syntax for the `paths:` frontmatter field, not YAML array syntax. | ||||||||||||||||||
|
|
||||||||||||||||||
| Background: The `paths:` field is parsed by an internal CSV parser that expects | ||||||||||||||||||
| a comma-separated string (e.g., `paths: "**/*.ts,**/*.tsx"`). When YAML array | ||||||||||||||||||
| syntax is used (e.g., `paths:\n - "**/*.ts"`), the YAML parser returns a | ||||||||||||||||||
| JavaScript array which the CSV parser iterates element-by-element instead of | ||||||||||||||||||
| character-by-character, silently producing invalid globs that match nothing. | ||||||||||||||||||
|
|
||||||||||||||||||
| Related issues: | ||||||||||||||||||
| - https://github.com/anthropics/claude-code/issues/19377 | ||||||||||||||||||
| - https://github.com/anthropics/claude-code/issues/13905 | ||||||||||||||||||
|
|
||||||||||||||||||
| This hook runs as a PostToolUse hook for Write and Edit tools. When a rules | ||||||||||||||||||
| file is written or edited, it checks the frontmatter for broken `paths:` syntax | ||||||||||||||||||
| and warns the user. | ||||||||||||||||||
|
|
||||||||||||||||||
| Hook configuration (add to settings.json): | ||||||||||||||||||
|
|
||||||||||||||||||
| { | ||||||||||||||||||
| "hooks": { | ||||||||||||||||||
| "PostToolUse": [ | ||||||||||||||||||
| { | ||||||||||||||||||
| "matcher": "Write|Edit", | ||||||||||||||||||
| "hooks": [ | ||||||||||||||||||
| { | ||||||||||||||||||
| "type": "command", | ||||||||||||||||||
| "command": "python3 /path/to/rules_frontmatter_validator.py" | ||||||||||||||||||
| } | ||||||||||||||||||
| ] | ||||||||||||||||||
| } | ||||||||||||||||||
| ] | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| """ | ||||||||||||||||||
|
|
||||||||||||||||||
| import json | ||||||||||||||||||
| import re | ||||||||||||||||||
| import sys | ||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| def extract_frontmatter(content: str) -> str | None: | ||||||||||||||||||
| """Extract YAML frontmatter from a markdown file.""" | ||||||||||||||||||
| match = re.match(r"^---\s*\n(.*?)\n---", content, re.DOTALL) | ||||||||||||||||||
| return match.group(1) if match else None | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| def check_paths_syntax(frontmatter: str) -> list[str]: | ||||||||||||||||||
| """Check for broken paths:/globs: syntax in frontmatter.""" | ||||||||||||||||||
| issues = [] | ||||||||||||||||||
|
|
||||||||||||||||||
| for field in ("paths", "globs"): | ||||||||||||||||||
| # 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 | ||||||||||||||||||
| ): | ||||||||||||||||||
|
Comment on lines
+59
to
+62
|
||||||||||||||||||
| # 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): |
Copilot
AI
Feb 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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): |
Copilot
AI
Feb 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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, |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,85 @@ | ||||||
| # Rules Frontmatter Examples | ||||||
|
|
||||||
| Example Claude Code rules files demonstrating **correct** and **incorrect** frontmatter syntax for the `paths:` field. | ||||||
|
|
||||||
| > [!WARNING] | ||||||
| > The `paths:` field in rules frontmatter uses a **CSV string parser** internally. YAML array syntax and JSON inline array syntax will cause rules to silently fail to match any files — the rule content loads but never applies. | ||||||
|
|
||||||
| ## Correct Syntax | ||||||
|
|
||||||
| ### Single glob (unquoted) | ||||||
|
|
||||||
| ```yaml | ||||||
| --- | ||||||
| paths: "**/*.ts" | ||||||
|
||||||
| paths: "**/*.ts" | |
| paths: **/*.ts |
Copilot
AI
Feb 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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).
Copilot
AI
Feb 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| --- | ||
| description: Python coding conventions | ||
| globs: "**/*.py,**/*.pyi" | ||
| --- | ||
|
|
||
| # Python Style | ||
|
|
||
| - Follow PEP 8 conventions | ||
| - Use type annotations on all function signatures | ||
| - Use `dataclass(frozen=True)` for immutable data structures |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| --- | ||
| description: TypeScript coding conventions | ||
| paths: "**/*.ts,**/*.tsx" | ||
| --- | ||
|
|
||
| # TypeScript Style | ||
|
|
||
| - Use `const` by default, `let` only when reassignment is needed | ||
| - Prefer `interface` over `type` for object shapes | ||
| - Use strict TypeScript configuration |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extract_frontmatteronly 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?\nin the regex (and optionally allowing---\s*\r?\nfor both delimiters).