aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--claude-rules/emacs.md5
-rw-r--r--claude-rules/org-tables.md5
-rw-r--r--claude-rules/todo-format.md5
-rwxr-xr-xscripts/lint.sh36
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