claude-config-lint
v0.1.0
Published
Static linter for Claude Code instruction hierarchies: zombie SUPERSEDED-BY references, dead markers, skill-description routing collisions.
Maintainers
Readme
claude-config-lint
Static-analysis linter for Claude Code instruction hierarchies. It reads your global and
project CLAUDE.md files plus every SKILL.md in the tree (project **/skills/*/ and
global ~/.claude/skills/) and reports three classes of silent failure:
- ZOMBIE-DOC — a doc retired with a
SUPERSEDED-BYmarker that a live skill still references. The agent reads a dead doc and follows dead rules. Full retirement is an error; a marker that scopes itself ("naming section only — rest is LIVE") is a warning. - DEAD-MARKER — a
SUPERSEDED-BYmarker whose named successor file doesn't exist. Every reader dead-ends. - SKILL-COLLISION — two skill descriptions whose trigger sets overlap enough that the model may route to the wrong skill. Lexical scoring (quoted-trigger extraction + token Jaccard), no LLM call — and it stays silent on intentional cede clauses ("not for risk screening — use risk-screen"), which are cross-references, not collisions.
Runs locally, no API key, machine-readable output for CI.
Usage
bunx claude-config-lint # lint cwd + your global ~/.claude
bunx claude-config-lint <root> # lint a specific tree
bunx claude-config-lint --no-global --json # project only, JSON findingsFindings print as severity path:line RULE message. Exit codes: 0 clean (warnings are
advisory by default), 1 warnings with --strict, 2 any error.
Config (optional, claude-config-lint.json or --config <path> — see
claude-config-lint.example.json): roots, extra content dirs to scan for rule docs,
global-dir override, per-rule enable/severity/threshold.
What counts as a marker
A line in the doc's first 5 lines that starts with SUPERSEDED-BY (leading >
blockquote and ⚠ allowed):
> ⚠ SUPERSEDED-BY: docs/new-sop.md — whole doc retired.Mid-sentence mentions ("always follow SUPERSEDED-BY headers") never match — that false
positive is a regression fixture, learned from the bash script this tool grew out of.
Scope words inside the marker's own blockquote lines ("brand refs only", "rest of this doc
is LIVE") mark a PARTIAL retirement, which downgrades zombie references to warnings.
Templated references like channels/<channel>/SOP.md are never resolved.
How it compares
Existing agent-config linters score files for structure, clarity, and security —
AgentLinter (8-dimension scoring, secret
patterns) and claudelint (116 rules across
CLAUDE.md size/imports, skill frontmatter/security, settings/hooks/MCP schemas — verified
against its actual list-rules output, 2026-08-12). Neither models the instruction
lifecycle: nothing checks SUPERSEDED-BY retirement chains, and claudelint's trigger
checks (skill-description-missing-trigger, skill-overly-generic-name) are per-file —
there is no pairwise trigger-overlap detection between skills. That's the gap this tool
covers: it models how Claude Code actually loads and routes instructions.
Measured on a real 45-skill install (kept local, never committed — see below): all 45 descriptions parse, top pairwise Jaccard 0.264 (below threshold), zero false collisions on 11 intentional cede-clause cross-references.
Fixtures and privacy
The committed test corpus is a synthetic structural clone — same file shapes,
frontmatter grammar, and marker conventions as a real install, invented content. No real
install snapshot is ever committed; .ci/leak-lint.sh greps every committed fixture
against a hashed denylist on every CI run and fails the build loudly if real-install
strings ever land.
Development
bun install
bun test # fixture-driven; every rule proven against stub and always-fires impls
bash .ci/leak-lint.shRules are pure functions (model) => Finding[] in src/rules/ — one file per rule, the
seam a future rule (e.g. cross-file precedence conflicts, planned v0.2) plugs into.
MIT.
