A file under skills/<name>/references/ sits one directory deeper than SKILL.md, so a link to a shared checklist needs ../../../references/ from there. The validator only read SKILL.md, which left that off-by-a-level mistake unguarded in the one place it can now happen (hardening-patterns.md on main, optimization-patterns.md in #580).
The rule is unchanged: only references/*.md links, fenced blocks exempt, paths that do not exist yet ignored. The file set grows to include the markdown files directly inside each skill's references/ directory, and every link resolves from the file that contains it. The failure hint names the three-level prefix when the violation is in a reference file.
Reuse the skill linter's fence parser so inline backticks cannot hide broken references and trailing text cannot close a code fence. Cover both failures with LF and CRLF regression cases, preserving diagnostic line numbers.
`validate-reference-links.js` scans every line of a SKILL.md, so a
`references/…md` path shown inside a fenced code block is treated as a link
to resolve. The rule this validator enforces therefore cannot be documented:
its own failure message tells authors to write `../../references/<file>.md`
rather than `references/<file>.md`, and a skill showing that contrast as an
example fails the check that is explaining it.
Reproduced on main by appending a fenced markdown example to
using-agent-skills/SKILL.md — exit 1, with the guidance text printed
underneath the very link it had just rejected.
Lines inside a fence are now skipped. The scan is line-by-line rather than
one regex over the document so the three CommonMark fence forms all hold:
tilde fences, up to three spaces of indent, and a closing fence longer than
its opener — and a ``` line inside a ~~~ block does not close it.
Line numbers are preserved by collecting fenced line indices and skipping
them during the scan, rather than stripping the text first, so the L<n> in
an error still points at the real line.
Deliberately local rather than shared with scripts/lib/skill-lint.js: that
module's stripFencedCodeBlocks is the single regex with the gaps described
in #437, and #444 is already rewriting it. Forking a rewrite in flight
costs more than the duplication; once #444 lands the two should collapse
into one helper under scripts/lib/.
Three tests added. All three fail against the current validator; the third
pins that a genuinely broken link outside a fence is still reported, so the
exemption cannot silently swallow real breakage.
The path fix has no regression guard: nothing in CI resolves references/
links, so all 18 were broken while CI stayed green. validate-artifact-
paths.js is scoped to spec/plan/todo artifacts and says in its own header
that it is not a general markdown path linter.
Add validate-reference-links.js, which resolves every `references/*.md`
link in skills/*/SKILL.md against that skill's own directory. This accepts
both conventions in CLAUDE.md: shared checklists reached via
../../references/, and a skill's own colocated references/ directory.
Scope stays narrow on purpose. Skills legitimately name paths that do not
exist yet -- tasks/todo.md, PERF.md, docs/ideas/[idea-name].md -- and a
general markdown linter would fail the build on them. A test pins that.
Proven against the pre-fix tree: 18 error(s), exit 1, matching the 18
links fixed in the previous commit. After the fix: 0 error(s), exit 0.
7 unit tests cover the regression itself, colocated references/,
markdown-link syntax, a renamed target, multiple violations in one skill,
and the non-reference paths that must be ignored. Wired into the
validate-skills job, alongside the other skill-content checks.
Known limitation: fenced code blocks are not stripped, so a SKILL.md that
documents the anti-pattern inside a fence would be flagged. Nothing does
today. Sharing stripFencedCodeBlocks looks right once #444 lands.
Refs #468
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>