diff options
| -rw-r--r-- | claude-rules/emacs.md | 5 | ||||
| -rw-r--r-- | claude-rules/org-tables.md | 5 | ||||
| -rw-r--r-- | claude-rules/todo-format.md | 5 | ||||
| -rwxr-xr-x | scripts/lint.sh | 36 |
4 files changed, 50 insertions, 1 deletions
diff --git a/claude-rules/emacs.md b/claude-rules/emacs.md index 2c3b729..846888d 100644 --- a/claude-rules/emacs.md +++ b/claude-rules/emacs.md @@ -1,3 +1,8 @@ +--- +paths: + - "**/*.el" +--- + # Working With Craig's Running Emacs Applies to: `**/*.el` (and any task that edits Craig's Emacs configuration) diff --git a/claude-rules/org-tables.md b/claude-rules/org-tables.md index 1b70085..bc9d27d 100644 --- a/claude-rules/org-tables.md +++ b/claude-rules/org-tables.md @@ -1,3 +1,8 @@ +--- +paths: + - "**/*.org" +--- + # Org Table Standard Applies to: `**/*.org` diff --git a/claude-rules/todo-format.md b/claude-rules/todo-format.md index dc5e3b3..58570b1 100644 --- a/claude-rules/todo-format.md +++ b/claude-rules/todo-format.md @@ -1,3 +1,8 @@ +--- +paths: + - "**/*.org" +--- + # Todo Entry Format Applies to: `**/*.org` (org-mode todo and inbox files) diff --git a/scripts/lint.sh b/scripts/lint.sh index 82f3b34..ca6abbd 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -21,10 +21,22 @@ warn() { errors=$((errors + 1)) } +# Print a rule file's body with any leading YAML frontmatter stripped, so the +# structural checks below see the Markdown regardless of whether the file +# carries a `paths:` block. Claude Code reads the frontmatter; the heading check +# should not care that it is there. +md_body() { + awk 'NR==1 && $0=="---" {fm=1; next} + fm && $0=="---" {fm=0; next} + !fm {print}' "$1" +} + check_md_heading() { local f="$1" [ -f "$f" ] || return 0 - if ! head -1 "$f" | grep -q '^# '; then + # First non-blank line, so a blank separator after frontmatter doesn't read as + # a missing heading. + if ! md_body "$f" | grep -m1 -v '^[[:space:]]*$' | grep -q '^# '; then warn "$f — missing top-level heading" fi } @@ -37,6 +49,27 @@ check_md_applies_to() { fi } +# A rule whose prose declares a file-type scope must also carry `paths:` +# frontmatter, or Claude Code loads it into every session regardless of what the +# prose says. Three rules declared a narrow scope this way and were loaded +# universally for as long as they shipped, because the declaration lived only in +# a line the loader never reads. The prose is for the human; the frontmatter is +# what actually scopes the load. Keep them saying the same thing. +check_md_paths_frontmatter() { + local f="$1" applies + [ -f "$f" ] || return 0 + applies=$(grep -m1 '^Applies to:' "$f" 2>/dev/null) || return 0 + # A scope naming a concrete extension (`**/*.org`, `**/*.el`) is path-scopable. + # A bare `**/*` is genuinely universal and wants no frontmatter. + case "$applies" in + *'**/*.'*) + if ! head -1 "$f" | grep -q '^---$'; then + warn "$f — declares a file-type scope in prose but has no 'paths:' frontmatter; it loads in every session" + fi + ;; + esac +} + check_hook() { local f="$1" [ -f "$f" ] || return 0 @@ -81,6 +114,7 @@ for f in claude-rules/*.md; do [ -f "$f" ] || continue check_md_heading "$f" check_md_applies_to "$f" + check_md_paths_frontmatter "$f" done # Per-language rule files |
