diff options
Diffstat (limited to 'scripts/lint.sh')
| -rwxr-xr-x | scripts/lint.sh | 47 |
1 files changed, 46 insertions, 1 deletions
diff --git a/scripts/lint.sh b/scripts/lint.sh index ae30aa5..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 @@ -99,6 +133,9 @@ for claude_md in languages/*/CLAUDE.md; do check_md_heading "$claude_md" done +# Language-neutral default CLAUDE.md (install-lang's fallback when a bundle ships none) +[ -f languages/default-CLAUDE.md ] && check_md_heading languages/default-CLAUDE.md + # Hook scripts for h in languages/*/claude/hooks/*.sh languages/*/githooks/*; do [ -f "$h" ] || continue @@ -111,6 +148,14 @@ for s in scripts/*.sh; do check_hook "$s" done +# Scripts `make install` symlinks onto PATH. Extensionless by convention, so +# they need their own glob — scripts/*.sh above never matched them, which left +# the most exposed shell in the repo as the only shell with no gate over it. +for s in claude-templates/bin/*; do + [ -f "$s" ] || continue + check_hook "$s" +done + # Markdown link validation across rules and skills for f in claude-rules/*.md */SKILL.md; do [ -f "$f" ] || continue |
