feat(plugin): command metadata, scoped skills, local-tier support, layered hooks, Stop audit

Wave 3 - adoption hardening. Patterns adapted (in original prose, with
attribution) from MIT-licensed shanraisshan/claude-code-best-practice.

Commands (command/enhance-claude-md.md, command/sync-claude-md.md):
- Add allowed-tools / disallowedTools / argument-hint / when_to_use so the
  commands auto-suggest in the slash menu and avoid permission prompts.
- disallowedTools blocks WebFetch + WebSearch on both commands.
- Drop the previous broken hooks block (array-of-{matcher, commands} shape
  did not match canonical schema; was never firing).

Skills:
- skill/karpathy-guidelines/SKILL.md: paths: glob over 23 code-file
  extensions, so the guardrails auto-load only when editing source, not
  markdown or data.
- skill/SKILL.md: model: haiku, effort: medium, paths: scoped to CLAUDE.md
  + AGENTS.md + .claude/rules/*.md so validator/generator passes run
  cheaply without changing the user-facing model.

CLAUDE.local.md personal tier:
- skill/validator.py BestPracticesValidator now accepts filename=; any
  *.local.md basename waives the 150-line cap.
- hooks/validate-claude-md.py reads the exempt suffix from hooks-config.
- .gitignore covers CLAUDE.local.md, **/CLAUDE.local.md,
  .claude/settings.local.json, hooks/hooks-config.local.json.

Layered hook config:
- hooks/hooks-config.json: committed defaults
  (validateClaudeMd.enabled/maxLines/exemptFilenameSuffix/exitCodeOnViolation,
  stopAuditLine.enabled).
- hooks/validate-claude-md.py merges hooks-config.json +
  hooks-config.local.json key-by-key; honours enabled=false (silent
  exit 0), configurable cap, configurable exit code.

Stop audit hook:
- hooks/audit-claude-md.py walks the project tree, prints one stderr
  line: total tracked / OVER cap / near cap (>=80%). Respects
  stopAuditLine.enabled from config.
- hooks/hooks.json registers Stop event with matcher "".

Guardian fail-closed contract:
- agent/claude-md-guardian.md Safety & Validation section now explicitly
  requires Skill-tool invocation (no inline paraphrase of SKILL.md),
  abort on missing validated output, never auto-commit, and respect
  local hook config.

Verified (8/8 smoke tests):
- Both commands parse with new fields and no broken hooks block.
- karpathy paths: 23 globs, includes .py/.ts/.go/.rs.
- skill model=haiku effort=medium with CLAUDE.md path scope.
- Validator: *.local.md (300 lines) -> pass; CLAUDE.md (300) -> fail;
  legacy ctor without filename -> default behavior preserved.
- hooks-config.json valid; validateClaudeMd.enabled=true, maxLines=150.
- Hook validator: default rc=2 on bloated, rc=0 when local override
  disables it, rc=0 on *.local.md (exempt).
- Stop hook entry present; audit script: rc=0 with "5 CLAUDE.md tracked".
- Regression: large-fullstack root still 52 lines with chain imports.
This commit is contained in:
Claude
2026-05-19 01:07:11 +00:00
parent 0a34178e22
commit e33fa8326b
12 changed files with 317 additions and 36 deletions
+8
View File
@@ -85,3 +85,11 @@ subagent-claude-md-guardian/
# Optional project files # Optional project files
PROJECT_SUMMARY.md PROJECT_SUMMARY.md
# ClaudeForge: personal / machine-local overrides outside the chained tree
CLAUDE.local.md
**/CLAUDE.local.md
.claude/settings.local.json
# ClaudeForge: per-developer hook overrides (e.g. disable validator locally)
hooks/hooks-config.local.json
+12
View File
@@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added (wave 3 — adoption hardening)
- **Command discovery metadata** (`command/enhance-claude-md.md`, `command/sync-claude-md.md`): both commands now declare `allowed-tools`, `disallowedTools` (blocks `WebFetch`/`WebSearch`), `argument-hint`, and `when_to_use` so Claude Code can auto-suggest and zero-prompt them.
- **Path-scoped Karpathy guidelines** (`skill/karpathy-guidelines/SKILL.md`): `paths:` glob on code-file extensions (`*.py`, `*.ts`, `*.go`, `*.rs`, etc.) so the guardrails load only when editing code, not when editing markdown or data.
- **Cheaper skill execution** (`skill/SKILL.md`): `model: haiku`, `effort: medium`, and `paths:` scoping the skill to CLAUDE.md / AGENTS.md / `.claude/rules/*.md` so validator + generator passes run cheaply without affecting the user-facing model.
- **`CLAUDE.local.md` personal tier**: `validator.BestPracticesValidator` now accepts `filename=` and waives the 150-line cap for any `*.local.md` file. `hooks/validate-claude-md.py` is exempt-suffix aware too. `.gitignore` excludes `CLAUDE.local.md`, `**/CLAUDE.local.md`, `.claude/settings.local.json`, and `hooks/hooks-config.local.json`.
- **Layered hook config** (`hooks/hooks-config.json` shared + `hooks/hooks-config.local.json` gitignored): `validate-claude-md.py` merges the two and honours `validateClaudeMd.enabled: false`, `maxLines`, `exemptFilenameSuffix`, and `exitCodeOnViolation`. Teams can opt out per developer without forking the shipped config.
- **`Stop` audit hook** (`hooks/audit-claude-md.py` + entry in `hooks/hooks.json`): prints a 1-line summary to stderr at session end — total CLAUDE.md tracked, count over the cap, count near it — so users see drift before the session's context is lost.
- **Fail-closed contract on guardian** (`agent/claude-md-guardian.md` Safety & Validation section): the guardian now states it invokes `claude-md-enhancer` exclusively through the Skill tool (never paraphrases SKILL.md content), aborts on missing validated output, never auto-commits, and respects the local hook config.
Patterns adapted (with attribution and in original prose) from the MIT-licensed [shanraisshan/claude-code-best-practice](https://github.com/shanraisshan/claude-code-best-practice).
### Fixed ### Fixed
- **Guardian agent hook frontmatter** (`agent/claude-md-guardian.md`): rewritten from the array-of-objects shape (`hooks: [{ event, commands }]`) to Anthropic's canonical keyed-object shape (`hooks: { EventName: [{ matcher, hooks: [{ type: "command", command }] }] }`). The previous shape did not match the documented schema, so the guardian's hooks did not fire. ([docs](https://code.claude.com/docs/en/hooks)) - **Guardian agent hook frontmatter** (`agent/claude-md-guardian.md`): rewritten from the array-of-objects shape (`hooks: [{ event, commands }]`) to Anthropic's canonical keyed-object shape (`hooks: { EventName: [{ matcher, hooks: [{ type: "command", command }] }] }`). The previous shape did not match the documented schema, so the guardian's hooks did not fire. ([docs](https://code.claude.com/docs/en/hooks))
+10 -2
View File
@@ -241,8 +241,14 @@ The slash command can invoke me:
## Safety & Validation ## Safety & Validation
**Critical Validation Rule**: **Fail-closed contract** (non-negotiable):
"Always validate your output against official native examples before declaring complete."
- I invoke `claude-md-enhancer` exclusively through the **Skill tool**. I never read its `SKILL.md` body and act on a paraphrase of it — paraphrase drift is the most common silent-degradation mode for auto-CLAUDE.md tooling.
- If the skill returns no validated output (missing required sections, validator status ≠ `pass`, or any thrown exception), I **abort the run** and leave the existing CLAUDE.md tree untouched. Partial writes are worse than stale documentation.
- I never commit on my own. Every change lands in the working tree only; the user reviews `git diff` and chooses when to commit.
- I respect `hooks/hooks-config.local.json`. If a developer has disabled the validator locally, I treat the cap as advisory for that machine but still warn on the Stop hook.
**Critical Validation Rule**: validate every emitted file against the reference templates in `skill/examples/` and the canonical schema before declaring success.
**My validation checklist**: **My validation checklist**:
- ✅ Project Structure diagram present - ✅ Project Structure diagram present
@@ -250,6 +256,8 @@ The slash command can invoke me:
- ✅ Architecture section reflects actual patterns - ✅ Architecture section reflects actual patterns
- ✅ Tech Stack lists all major dependencies - ✅ Tech Stack lists all major dependencies
- ✅ Common Commands match package.json scripts - ✅ Common Commands match package.json scripts
- ✅ Every emitted CLAUDE.md ≤ 150 lines (cap waived only for `*.local.md`)
- ✅ Every sub-CLAUDE.md back-links to root; root has matching `@`-imports
## Installation ## Installation
+25 -9
View File
@@ -1,18 +1,34 @@
--- ---
description: Initialize or enhance CLAUDE.md files using the claude-md-enhancer skill with interactive workflow and 100% native format compliance description: Initialize or enhance a CLAUDE.md (and chained sub-CLAUDE.md files) for the current project using the claude-md-enhancer skill. Delegates deep codebase scans to the Explore subagent and stays within the 150-line cap.
argument-hint: "[--init | --enhance | <path-to-CLAUDE.md>]"
when_to_use: |
Use whenever a project has no CLAUDE.md, when an existing one is over 150 lines,
when an /init result needs to be hardened against context bloat, or when a repo
already uses AGENTS.md / .cursorrules / .windsurfrules and you want a Claude-
aware root that chains to them via @-imports instead of overwriting.
allowed-tools:
- Read
- Edit
- Write
- Glob
- Grep
- Skill
- "Bash(ls:*)"
- "Bash(find:*)"
- "Bash(git status:*)"
- "Bash(git diff:*)"
- "Bash(wc:*)"
disallowedTools:
- WebFetch
- WebSearch
permissions: permissions:
allow: allow:
- Bash(ls:*) - "Bash(ls:*)"
- Bash(find:*) - "Bash(find:*)"
- Bash(git status:*) - "Bash(git status:*)"
- Read - Read
- Glob - Glob
- Skill - Skill
hooks:
- matcher: ""
once: true
commands:
- echo "Starting CLAUDE.md enhancement workflow"
--- ---
# CLAUDE.md Enhancer Command # CLAUDE.md Enhancer Command
+32 -14
View File
@@ -1,26 +1,44 @@
--- ---
description: Walk every CLAUDE.md in the project, prune stale references, enforce the 150-line cap, and re-chain root ↔ subdirectory files. description: Walk every CLAUDE.md in the project, prune stale references (removed deps, deleted paths, broken modular links), enforce the 150-line cap by splitting into sub-files, and repair the root ↔ subdirectory chain (markdown links + @path imports).
argument-hint: "[--dry-run | --paths-only | <directory>]"
when_to_use: |
Run after refactors, dependency changes, deleted directories, or when any single
CLAUDE.md is near the 150-line cap. Also run before cutting a release so the
documentation tag-snapshot is truthful.
allowed-tools:
- Read
- Edit
- Write
- Glob
- Grep
- Skill
- "Bash(ls:*)"
- "Bash(find:*)"
- "Bash(git status:*)"
- "Bash(git diff:*)"
- "Bash(wc:*)"
- "Bash(grep:*)"
- "Bash(cat:*)"
- "Bash(test:*)"
disallowedTools:
- WebFetch
- WebSearch
permissions: permissions:
allow: allow:
- Bash(ls:*) - "Bash(ls:*)"
- Bash(find:*) - "Bash(find:*)"
- Bash(git status:*) - "Bash(git status:*)"
- Bash(git diff:*) - "Bash(git diff:*)"
- Bash(wc:*) - "Bash(wc:*)"
- Bash(grep:*) - "Bash(grep:*)"
- Bash(cat:*) - "Bash(cat:*)"
- Bash(test:*) - "Bash(test:*)"
- Read - Read
- Edit - Edit
- Write - Write
- Glob - Glob
- Grep - Grep
- Skill - Skill
hooks:
- matcher: ""
once: true
commands:
- echo "Starting CLAUDE.md sync workflow"
--- ---
# /sync-claude-md — CLAUDE.md Sync & Cleanup # /sync-claude-md — CLAUDE.md Sync & Cleanup
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""ClaudeForge Stop hook: print a 1-line CLAUDE.md health summary.
Walks the project tree from ``CLAUDE_PROJECT_DIR`` (or the cwd) and prints a
single line to stderr summarising how many CLAUDE.md files exist, how close
they are to the 150-line cap, and whether any are over. Designed to be the
last signal before a session's context is lost — drift visible to the user
without forcing them to run ``/sync-claude-md`` blindly.
Honours ``hooks/hooks-config.json`` and ``hooks/hooks-config.local.json``:
when ``stopAuditLine.enabled`` is ``false``, this script exits silently.
"""
from __future__ import annotations
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_MAX_LINES = 150
def _load_config() -> dict:
cfg: dict = {}
for name in ("hooks-config.json", "hooks-config.local.json"):
path = os.path.join(HERE, name)
if not os.path.exists(path):
continue
try:
with open(path, encoding="utf-8") as fh:
data = json.load(fh)
except (OSError, json.JSONDecodeError):
continue
cfg.update(data.get("stopAuditLine") or {})
cfg.setdefault("maxLines", (data.get("validateClaudeMd") or {}).get("maxLines"))
return cfg
def _project_root() -> str:
return os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd()
def _iter_claude_md(root: str):
skip_dirs = {".git", "node_modules", ".venv", "venv", "dist", "build", "vendor"}
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in skip_dirs]
for name in filenames:
if name == "CLAUDE.md":
yield os.path.join(dirpath, name)
def main() -> int:
cfg = _load_config()
if cfg.get("enabled") is False:
return 0
cap = int(cfg.get("maxLines") or DEFAULT_MAX_LINES)
warn_at = max(1, int(cap * 0.8))
total = 0
over = 0
near = 0
for path in _iter_claude_md(_project_root()):
try:
with open(path, encoding="utf-8") as fh:
lines = sum(1 for _ in fh)
except OSError:
continue
total += 1
if lines > cap:
over += 1
elif lines >= warn_at:
near += 1
if total == 0:
return 0
suffix = ""
if over:
suffix = f"{over} OVER {cap}-line cap; run /sync-claude-md"
elif near:
suffix = f"{near} near cap ({warn_at}+)"
print(f"ClaudeForge: {total} CLAUDE.md tracked{suffix}", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())
+12
View File
@@ -0,0 +1,12 @@
{
"$comment": "ClaudeForge shared hook config. Commit this file. Per-developer overrides go in hooks/hooks-config.local.json (gitignored).",
"validateClaudeMd": {
"enabled": true,
"maxLines": 150,
"exemptFilenameSuffix": ".local.md",
"exitCodeOnViolation": 2
},
"stopAuditLine": {
"enabled": true
}
}
+12
View File
@@ -24,6 +24,18 @@
} }
] ]
} }
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PROJECT_DIR}}/hooks/audit-claude-md.py",
"timeout": 10
}
]
}
] ]
} }
} }
+43 -7
View File
@@ -17,7 +17,32 @@ import json
import os import os
import sys import sys
MAX_LINES = 150 DEFAULT_MAX_LINES = 150
DEFAULT_EXEMPT_SUFFIX = ".local.md"
DEFAULT_VIOLATION_RC = 2
def _load_config() -> dict:
"""Merge ``hooks-config.json`` and optional ``hooks-config.local.json``.
Local file overrides the shared one key-by-key inside ``validateClaudeMd``.
Missing files are silently ignored — the script falls back to defaults.
"""
here = os.path.dirname(os.path.abspath(__file__))
shared = os.path.join(here, "hooks-config.json")
local = os.path.join(here, "hooks-config.local.json")
cfg: dict = {}
for path in (shared, local):
if not os.path.exists(path):
continue
try:
with open(path, encoding="utf-8") as fh:
data = json.load(fh)
except (OSError, json.JSONDecodeError):
continue
validate_block = data.get("validateClaudeMd") or {}
cfg.update(validate_block)
return cfg
def _candidate_paths(payload: dict) -> list[str]: def _candidate_paths(payload: dict) -> list[str]:
@@ -42,9 +67,13 @@ def _candidate_paths(payload: dict) -> list[str]:
return paths return paths
def _is_claude_md(path: str) -> bool: def _is_claude_md(path: str, exempt_suffix: str) -> bool:
base = os.path.basename(path) base = os.path.basename(path)
return base == "CLAUDE.md" or base.endswith(".claude/rules") or "/.claude/rules/" in path # Personal-tier overrides (CLAUDE.local.md and any matching suffix) are
# exempt from the cap — they live outside the chained team-shared tree.
if base.endswith(exempt_suffix):
return False
return base == "CLAUDE.md" or "/.claude/rules/" in path
def main() -> int: def main() -> int:
@@ -59,16 +88,23 @@ def main() -> int:
except json.JSONDecodeError: except json.JSONDecodeError:
return 0 return 0
cfg = _load_config()
if cfg.get("enabled") is False:
return 0
max_lines = int(cfg.get("maxLines", DEFAULT_MAX_LINES))
exempt_suffix = str(cfg.get("exemptFilenameSuffix", DEFAULT_EXEMPT_SUFFIX))
violation_rc = int(cfg.get("exitCodeOnViolation", DEFAULT_VIOLATION_RC))
violations: list[tuple[str, int]] = [] violations: list[tuple[str, int]] = []
for path in _candidate_paths(payload): for path in _candidate_paths(payload):
if not _is_claude_md(path) or not os.path.exists(path): if not _is_claude_md(path, exempt_suffix) or not os.path.exists(path):
continue continue
try: try:
with open(path, encoding="utf-8") as fh: with open(path, encoding="utf-8") as fh:
line_count = sum(1 for _ in fh) line_count = sum(1 for _ in fh)
except OSError: except OSError:
continue continue
if line_count > MAX_LINES: if line_count > max_lines:
violations.append((path, line_count)) violations.append((path, line_count))
if not violations: if not violations:
@@ -76,11 +112,11 @@ def main() -> int:
for path, line_count in violations: for path, line_count in violations:
print( print(
f"ClaudeForge: {path} is {line_count} lines (cap is {MAX_LINES}). " f"ClaudeForge: {path} is {line_count} lines (cap is {max_lines}). "
"Run /sync-claude-md to split into chained sub-files.", "Run /sync-claude-md to split into chained sub-files.",
file=sys.stderr, file=sys.stderr,
) )
return 2 return violation_rc
if __name__ == "__main__": if __name__ == "__main__":
+22 -3
View File
@@ -1,6 +1,25 @@
--- ---
name: claude-md-enhancer name: claude-md-enhancer
description: Analyzes, generates, and enhances CLAUDE.md files for any project type using best practices, modular architecture support, and tech stack customization. Use when setting up new projects, improving existing CLAUDE.md files, or establishing AI-assisted development standards. description: Analyzes, generates, and enhances CLAUDE.md files for any project type using best practices, modular architecture support, and tech stack customization. Use when setting up new projects, improving existing CLAUDE.md files, or establishing AI-assisted development standards.
model: haiku
effort: medium
paths:
- "**/CLAUDE.md"
- "**/CLAUDE.local.md"
- "**/AGENTS.md"
- "**/.cursorrules"
- "**/.windsurfrules"
- "**/.claude/rules/*.md"
allowed-tools:
- Read
- Write
- Edit
- Glob
- Grep
- "Bash(ls:*)"
- "Bash(find:*)"
- "Bash(git:*)"
- "Bash(wc:*)"
permissions: permissions:
allow: allow:
- Read - Read
@@ -8,9 +27,9 @@ permissions:
- Edit - Edit
- Glob - Glob
- Grep - Grep
- Bash(ls:*) - "Bash(ls:*)"
- Bash(find:*) - "Bash(find:*)"
- Bash(git:*) - "Bash(git:*)"
--- ---
# CLAUDE.md File Enhancer # CLAUDE.md File Enhancer
+28
View File
@@ -2,6 +2,34 @@
name: karpathy-guidelines name: karpathy-guidelines
description: Behavioral guardrails for LLM-assisted coding. Use when writing, reviewing, or refactoring code in any project to avoid overcomplication, keep changes surgical, surface assumptions early, and execute against verifiable success criteria. description: Behavioral guardrails for LLM-assisted coding. Use when writing, reviewing, or refactoring code in any project to avoid overcomplication, keep changes surgical, surface assumptions early, and execute against verifiable success criteria.
license: MIT license: MIT
paths:
- "**/*.py"
- "**/*.ts"
- "**/*.tsx"
- "**/*.js"
- "**/*.jsx"
- "**/*.go"
- "**/*.rs"
- "**/*.java"
- "**/*.kt"
- "**/*.rb"
- "**/*.php"
- "**/*.swift"
- "**/*.c"
- "**/*.cc"
- "**/*.cpp"
- "**/*.h"
- "**/*.hpp"
- "**/*.cs"
- "**/*.scala"
- "**/*.sh"
- "**/*.bash"
- "**/*.zsh"
- "**/*.sql"
allowed-tools:
- Read
- Glob
- Grep
permissions: permissions:
allow: allow:
- Read - Read
+26 -1
View File
@@ -66,18 +66,24 @@ class BestPracticesValidator:
} }
] ]
def __init__(self, content: str, project_context: Dict[str, Any] = None): def __init__(self, content: str, project_context: Dict[str, Any] = None, filename: str = None):
""" """
Initialize validator with CLAUDE.md content. Initialize validator with CLAUDE.md content.
Args: Args:
content: Full text content of CLAUDE.md file content: Full text content of CLAUDE.md file
project_context: Optional project context for advanced validation project_context: Optional project context for advanced validation
filename: Optional path or basename. When the basename ends with
``.local.md`` (e.g. ``CLAUDE.local.md``), the 150-line cap is
relaxed because the file is a personal/gitignored override
outside the chained team-shared tree.
""" """
self.content = content self.content = content
self.lines = content.split('\n') self.lines = content.split('\n')
self.line_count = len(self.lines) self.line_count = len(self.lines)
self.project_context = project_context or {} self.project_context = project_context or {}
self.filename = filename or ""
self.is_local_override = self.filename.endswith('.local.md')
def validate_all(self) -> Dict[str, Any]: def validate_all(self) -> Dict[str, Any]:
""" """
@@ -112,6 +118,25 @@ class BestPracticesValidator:
message = f"File length is appropriate ({self.line_count} lines)" message = f"File length is appropriate ({self.line_count} lines)"
severity = "info" severity = "info"
# CLAUDE.local.md (and any *.local.md sibling) is a personal,
# gitignored override outside the chained team-shared tree. Skip the
# 150-line cap — only flag underuse.
if self.is_local_override:
if self.line_count < self.MIN_LINES:
status = "fail"
message = f"Personal override is too short ({self.line_count} lines, minimum {self.MIN_LINES})"
severity = "low"
else:
message = f"Personal override ({self.line_count} lines, cap waived)"
return {
"check": "file_length",
"status": status,
"message": message,
"severity": severity,
"actual_value": self.line_count,
"expected_range": f"{self.MIN_LINES}+ lines (cap waived for *.local.md)",
}
if self.line_count > self.MAX_RECOMMENDED_LINES: if self.line_count > self.MAX_RECOMMENDED_LINES:
status = "fail" status = "fail"
message = f"File exceeds maximum recommended length ({self.line_count} > {self.MAX_RECOMMENDED_LINES} lines)" message = f"File exceeds maximum recommended length ({self.line_count} > {self.MAX_RECOMMENDED_LINES} lines)"