A bundled script named in a markdown heading was invisible to skill-resource-resolves, because the use-directive gate reads prose and a heading is not prose. An ATX heading now counts; the widening stops there, measured at 1 finding before and after over 28 SKILL.md files. Malformed frontmatter was regex-salvaged into the tool contract the lethal-trifecta and Safety ring read, so a unit whose allowed-tools a strict loader rejects scored as the narrow contract its author meant. A salvage may now only make the verdict worse, never better — the vendored corpus forced that nuance by going red when the first pass refused salvages outright. vigiles test reported a clean exit with zero assertions as a pass. vigiles/testing now counts checks per tier and the runner reports a fourth, non-fatal state. Silence stays a pass: undefined means nobody counted, 0 means counted and found nothing. No new CLI verb or flag. Every mechanism is deterministic. Direction is strictly more conservative: nothing can now pass that could not before.
3.3 KiB
frontmatter-valid
Flag a skill/agent whose --- frontmatter block exists but isn't valid YAML
— so its fields may not parse as the author intended. Same detector vigiles scan uses (the malformed flag from src/core/frontmatter-read.ts, a real
js-yaml parse).
Honest caveat — why this is warn, not error
A spec-compliant YAML parser (js-yaml) is stricter than some lenient
frontmatter loaders. The most common trip is a one-line description: that
contains a : colon, a quote, or an embedded <example> block:
description: Use this agent when: <example>user: "..." assistant: "..."</example>
That is invalid YAML (a plain scalar can't contain : ), so it's flagged — but
depending on the harness's loader it may still load (possibly with a literal
\n instead of a newline). Calibration against the mid-2026 sweep: 7% of
frontmatter blocks were flagged, concentrated in 2 repos (ananddtyagi,
MadAppGang) and 0 across the other 11 marketplaces — the signal correlates
with prose stuffed into frontmatter, but vigiles has not empirically
confirmed every flagged file fails to load.
So this ships warn by default (a nudge, exit 0) and scan surfaces it as an
informational ℹ note, not a structural defect (no leaderboard penalty).
Verify against your harness before setting "error". The unambiguous cases
(an unclosed [a, b, c, a tab indent, a duplicate key) are real breakage worth
fixing regardless.
What it flags
allowed-tools: [read, write # ✗ unclosed flow array — invalid YAML
description: a value with: a colon # ⚠ invalid plain scalar (may still load)
The file's other fields are still salvaged by the reader, so the rest of
scan/lint keeps working on a malformed file.
Except the tool contract, which is not salvaged for SCORING. A salvaged
allowed-tools: / tools: is a regex guess at a block a strict loader rejects, so
vigiles audit treats a malformed unit's contract as inherits-all rather than
grading the guess — see
lethal-trifecta.
The live PreToolUse rail still uses the salvage (something to enforce beats
nothing); only the score refuses it. So a broken block can cost a grade even when
the fields "look fine", and fixing the YAML restores both.
Configuration
{ "rules": { "frontmatter-valid": "warn" } }
Severity
| Value | Behavior |
|---|---|
"error" |
vigiles lint exits non-zero (2) — only set this once verified |
"warn" (default) |
Prints a warning, exits 0 |
false |
Skip the check |
Scope
skills/*/SKILL.md + agents/*.md (and .claude/...). The block must be at the
file start (after an optional BOM / a leading vigiles integrity comment) — a
--- horizontal rule in the body is never mistaken for frontmatter.
See also
- subagent-frontmatter — required fields + valid
model/color(a parsed frontmatter that's missing/typo'd a field, vs this rule's unparseable block).