skill-lint

A linter and dependency graph for a Claude Code skill library. It reads every SKILL.md it can find and reports what would send a model to the wrong skill, or to a file that is not there. No dependencies, standard library only.

What it found on a real library

Run over a real installation of 381 skills: 510 findings, of which 97 break something, 361 misfire, 8 are unclear and 44 are style. The graph has 177 references between skills and 245 skills at neither end of one.

The precision measurement below was taken on an earlier snapshot of the same library, at 355 skills and 648 findings. The 97 breaking findings are the same count in both runs. What fell was the overlap count, because a phrase counts as distinctive only when it appears in at most four descriptions across the whole library, so adding 26 skills made shared vocabulary less distinctive. That is the threshold working, and it is why the report prints the library's own distribution beside every threshold rather than the threshold alone.

The headline is in the official Vercel nextjs skill, which anybody can install. Every sibling link in its body points one directory too high:

See [font.md](./font.md) for:

while the file is at references/font.md. The same is true of image.md, scripts.md, directives.md and seven more. A model told to read them finds nothing, and a model that finds nothing usually improvises rather than reporting the gap.

Precision on broken references is 90%, hand-checked on a sample of 20 with a 95% Wilson interval of 69.9% to 97.2%. The interval is wide because the sample is small. Recall is not measured. Nothing here says how many broken references the linter never reported.

Severities, and what the exit code does

By default only breaks makes the command exit 1. A library with overlapping triggers is worth fixing and is not broken, and a linter that fails the build on a style finding gets wrapped in || true within a week, after which it reports nothing to anyone. --fail-on moves the line on purpose.

severitywhat it meanson the fixtures
breaksthe skill cannot do what it says, or cannot be reached at all10
misfiresthe skill works, and the wrong request reaches it or the right one does not4
uncleara model choosing between skills has too little to go on1
stylea real cost, paid on every request, that nothing is broken by1

The fixture library

Everything below is measured by running the linter over fixtures/library, an invented corpus committed to this repository. Nothing on this page comes from a real skill library, because the tool's input is private and a published page built from it would leak it.

29skills
16findings
12negative controls
4graph edges

Planted defects, and every one of them found

skillruleseveritywhat was planted
absolute-path-referenceabsolute path in a referencebreaksa backticked path rooted at /, which cannot resolve on another machine
broken-referencebroken path referencebreaksreferences/ exists beside SKILL.md and references/gone.md does not, so this is a true dangling pointer rather than prose about somebody else's project
demo-plugin:skills:missing-plugin-scriptbroken plugin-root referencebreaks${CLAUDE_PLUGIN_ROOT}/scripts/missing.sh in prose, inside a directory with .claude-plugin/, where no scripts directory exists
no-frontmatterfrontmatter unreadablebreaksthe file opens with a heading and has no --- block at all
stray-indented-linefrontmatter unreadablebreaksan indented line with no key above it to belong to, because a blank line closed the one before it. pyyaml refuses this shape too, which is why it was chosen over a wholly indented mapping: pyyaml accepts that one
unreadable-frontmatterfrontmatter unreadablebreaksa line that is neither `key: value` nor an indented continuation, which the parser refuses instead of dropping the line and reporting on what is left
missing-descriptionno descriptionbreaksfrontmatter carries a name and nothing else
nested-frontmatterno descriptionbreaksthe fields are nested one level under a key, so the file reads and the flat description a caller needs is genuinely absent
missing-nameno namebreaksfrontmatter carries a description and no name
nested-frontmatterno namebreaksthe same nesting, reported as the second of two specific findings rather than one generic refusal. Two findings that name the missing fields are more use than one saying the file could not be read, and refusing the file would also throw away flat fields where a real skill has them
no-trigger-descriptiondescription states no triggermisfiresthe description says what the skill is and never states a condition a request could match
duplicate-name-twin, vendored:duplicate-name-twinduplicate namemisfirestwo directories of the same name in different places, both claiming the frontmatter name duplicate-name-twin. One finding covers the pair
name-directory-mismatchname does not match its directorymisfiresfrontmatter says renamed-glaze-tool and the directory says name-directory-mismatch
overlap-kiln-alpha + overlap-kiln-betaoverlapping triggersmisfiresboth descriptions carry the phrases 'kiln firing' and 'firing schedules', which appear in no other description, so both are distinctive and the pair collides
short-descriptiondescription too short to choose onuncleara 40 character description, below the 60 character floor
oversized-bodybody very longstylea body of roughly 29 kB, above the 20000 byte ceiling

Negative controls, and every one of them silent

These sit exactly where a false positive would appear. They matter more than the defects: a suite proving only that planted defects are found would also pass on a linter that reported every skill as broken.

skillresultthe false positive it stands in for
clean-block-scalarsilenta folded block scalar description, which the parser must fold into one line before measuring it
clean-claude-md-mentionsilent./CLAUDE.md, a file of the surrounding repository that no skill ships
clean-domain-sibling-onesilentoverlaps(), paired with clean-domain-sibling-two: same subject, one shared phrase, which is below the threshold
clean-domain-sibling-twosilentoverlaps(), paired with clean-domain-sibling-one
clean-illustrative-referencesilent**Examples**: `references/whatever.md`, illustrative context, in a skill whose references/ directory does exist so the suppression is what does the work
clean-list-valuesilenta list under an empty key, which must stay two items and not become the string '- Read - Grep'
clean-nested-metadatasilenta nested block beside flat fields, which must not cost the file its readable name and description. Refusing here was the over-correction: nested metadata appears in 88 of 381 installed skills
clean-quoted-multilinesilenta quoted description opening on one line and closing several lines further down
clean-user-project-filessilentpackage.json, app.py and vercel.json, files of the surrounding project rather than bundle references
clean-working-referencesilenta markdown link to references/real.md, which is present on disk
clean-wrapped-scalarsilenta value that begins on the lines below its own key, which must be joined into one string and not mistaken for a nested mapping. This is the case pyyaml caught on a real skill
demo-plugin:skills:clean-fenced-examplesilent${CLAUDE_PLUGIN_ROOT}/scripts/build.sh inside a fenced block, documented syntax rather than a pointer

The reference graph

29 skills, 4 references between them, 23 at neither end of one. An edge is drawn only when the token names a skill that exists in the library, which is why npm and pnpm in a fixture body are not nodes.

A skill with no edges is not necessarily unused. Most skills are chosen by a model reading descriptions, not called by another skill, so isolation here is a fact about the text and not a verdict about the skill.