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.
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.
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.
| severity | what it means | on the fixtures |
|---|---|---|
| breaks | the skill cannot do what it says, or cannot be reached at all | 10 |
| misfires | the skill works, and the wrong request reaches it or the right one does not | 4 |
| unclear | a model choosing between skills has too little to go on | 1 |
| style | a real cost, paid on every request, that nothing is broken by | 1 |
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.
| skill | rule | severity | what was planted |
|---|---|---|---|
| absolute-path-reference | absolute path in a reference | breaks | a backticked path rooted at /, which cannot resolve on another machine |
| broken-reference | broken path reference | breaks | references/ 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-script | broken plugin-root reference | breaks | ${CLAUDE_PLUGIN_ROOT}/scripts/missing.sh in prose, inside a directory with .claude-plugin/, where no scripts directory exists |
| no-frontmatter | frontmatter unreadable | breaks | the file opens with a heading and has no --- block at all |
| stray-indented-line | frontmatter unreadable | breaks | an 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-frontmatter | frontmatter unreadable | breaks | a 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-description | no description | breaks | frontmatter carries a name and nothing else |
| nested-frontmatter | no description | breaks | the fields are nested one level under a key, so the file reads and the flat description a caller needs is genuinely absent |
| missing-name | no name | breaks | frontmatter carries a description and no name |
| nested-frontmatter | no name | breaks | the 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-description | description states no trigger | misfires | the description says what the skill is and never states a condition a request could match |
| duplicate-name-twin, vendored:duplicate-name-twin | duplicate name | misfires | two directories of the same name in different places, both claiming the frontmatter name duplicate-name-twin. One finding covers the pair |
| name-directory-mismatch | name does not match its directory | misfires | frontmatter says renamed-glaze-tool and the directory says name-directory-mismatch |
| overlap-kiln-alpha + overlap-kiln-beta | overlapping triggers | misfires | both descriptions carry the phrases 'kiln firing' and 'firing schedules', which appear in no other description, so both are distinctive and the pair collides |
| short-description | description too short to choose on | unclear | a 40 character description, below the 60 character floor |
| oversized-body | body very long | style | a body of roughly 29 kB, above the 20000 byte ceiling |
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.
| skill | result | the false positive it stands in for |
|---|---|---|
| clean-block-scalar | silent | a folded block scalar description, which the parser must fold into one line before measuring it |
| clean-claude-md-mention | silent | ./CLAUDE.md, a file of the surrounding repository that no skill ships |
| clean-domain-sibling-one | silent | overlaps(), paired with clean-domain-sibling-two: same subject, one shared phrase, which is below the threshold |
| clean-domain-sibling-two | silent | overlaps(), paired with clean-domain-sibling-one |
| clean-illustrative-reference | silent | **Examples**: `references/whatever.md`, illustrative context, in a skill whose references/ directory does exist so the suppression is what does the work |
| clean-list-value | silent | a list under an empty key, which must stay two items and not become the string '- Read - Grep' |
| clean-nested-metadata | silent | a 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-multiline | silent | a quoted description opening on one line and closing several lines further down |
| clean-user-project-files | silent | package.json, app.py and vercel.json, files of the surrounding project rather than bundle references |
| clean-working-reference | silent | a markdown link to references/real.md, which is present on disk |
| clean-wrapped-scalar | silent | a 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-example | silent | ${CLAUDE_PLUGIN_ROOT}/scripts/build.sh inside a fenced block, documented syntax rather than a pointer |
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.