mdformat (gfm-only) silently destroys SKILL.md YAML frontmatter
#1
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why: Running
uvx --with mdformat-gfm mdformat --wrap 80 <dir>over a Claude Code skill directory rewritesSKILL.md's YAML frontmatter into body text. The opening/closing---delimiters become a______…thematic break, and thename:/description:keys collapse into a single## name: … description: …ATX heading. Consequence: the skill has nonameand nodescription, so it cannot be routed to or loaded at all. It fails silently — the file still lints clean-ish and looks plausible; nothing errors.Reproduced this session on
oleks/claude-plugin-cicd-insights,skills/woodpecker-trigger-discipline/SKILL.md. Detected only because a subsequent markdownlint run reportedMD041 first-line-headingwith context"______________…", which prompted a re-read of the file head.Fix: the invocation needs
--with mdformat-frontmatterin addition to--with mdformat-gfm. Verified: with both plugins the file round-trips byte-identically and frontmatter is preserved.mdformat-gfmalone protects tables but NOT frontmatter — these are two separate hazards and the well-known one (tables) masks the other.Where it matters: any documented/instructed reformat step that touches
skills/**/SKILL.mdor agent definition files with frontmatter. The-gfm-only invocation is in active circulation as the recommended command.Acceptance:
--with mdformat-frontmatterskills/woodpecker-trigger-discipline/SKILL.mdfrontmatter verified intactRoot-cause follow-up: where the bad invocation actually comes from.
Searched for the source of the
-gfm-only recipe so it could be fixed rather than just documented. Findings:~/projects/claude-pluginsreferencesmdformatat all — not in any plugin, skill, command, or hook script.mdformateither. They runmarkdownlint, which is what detects the damage (MD041with context"______…") but never causes it. So the hook is not the propagation path.project_ci-lint-factbooks-premise-was-wrong-twice.mdrecordeduvx --with mdformat-gfm mdformat --wrap 80as the fix for the tables gotcha, with no mention of frontmatter. That file has now been corrected in place with an explicit warning, and the memory index carries the correction on its pointer line.Why this class of mistake is sticky: the tables gotcha is well known, so
--with mdformat-gfmlooks like the complete, informed invocation. Knowing about the first hazard is exactly what makes the truncated command feel correct — the fix for one silent corruption reads as protection against all of them.Residual risk this issue still tracks: nothing mechanical prevents a future session from running the two-plugin-short command. Options, if worth pursuing:
SKILL.mdor agent.mdloses its frontmatter — cheap, deterministic, and catches the damage at the only point where it matters. This is the strongest option: it does not depend on anyone remembering the right flags.CLAUDE.mdconventions section, next to the existing pre-push auto-format note.Option 1 is the real fix; option 2 only helps whoever reads it. Not implementing either here without direction, since both are outside this plugin.
Documentation fix landed; the mechanical guard is still open, and that is the part worth deciding on.
Two propagation paths have now been closed:
Session memory —
project_ci-lint-factbooks-premise-was-wrong-twice.mdrecorded the-gfm-only recipe as the fix for the tables gotcha, with no mention of frontmatter. That is what kept feeding the truncated command into new sessions. Corrected in place with an explicit warning, and flagged on the memory index line so a reader scanning the index cannot pick up the bad form.Workspace conventions —
~/projects/claude-plugins/CLAUDE.mdnow carries the full invocation next to the existing pre-push auto-format bullet, with the mechanism spelled out: the tables warning masks the frontmatter one, and knowing about-gfmis precisely what makes the two-plugin-short command feel informed. Applies to every plugin in the workspace, since they all ship frontmatter-bearingSKILL.md/agents/*.md.Also added there: the changed-files-only lint behaviour that produced oleks/claude-plugin-cicd-insights#3, since the two hazards bite in the same moment (hand-reformatting a file).
Still open — the real fix. Neither of the above prevents anything; they only help whoever reads them. The mechanical answer is a pre-push guard that fails when a tracked
SKILL.mdoragents/*.mdloses its frontmatter — cheap, deterministic, and it catches the damage at the only point that matters, regardless of which command produced it. It does not depend on anyone remembering flags.That guard belongs in the global hooks (
git-global-hooks), which are NixOS-managed and need a deploy, so it is out of scope for this repo and I have not implemented it. Leaving this issue open against that decision rather than closing on the docs fix — a documentation-only resolution to a silent-corruption bug is exactly the kind of close that looks done and is not.The guard already exists. It has never run.
Asked to implement this as a
claudecode-linterrule, I checked whether one was needed first. It is not —claudecode-linteralready shipsskill-md/valid-frontmatterandagent-md/valid-frontmatter, and they catch exactly this corruption. Verified against a purpose-built fixture:Correct rule, non-zero exit, both file types covered. Writing a new one would have duplicated it.
So why did a corrupted
SKILL.mdget committed and pushed with the detector sitting in the toolchain? The pre-push hook guards the whole slot on the binary being present:which claudecode-linterreturns nothing on emmett. It is not on PATH, appears nowhere in~/projects/servers/emmett/**/*.nix, and the linter repo ships no Nix expression (it is an npm package with adist/). Because the guard isif which(...), its absence produces no warning and no PASS token — indistinguishable from having run and passed.The evidence was in plain sight all session: every push printed
MDL ✅, one printedMDL ✅ BIO ✅ CPV ✅ TAG ⚠. NoCCLtoken in any of them. A missing token is the only signal, and only if you know to look for it.This is the third instance of one shape in this work — a check that silently no-ops and is therefore indistinguishable from a passing check. The other two were a paginated API read that looks complete, and a routing test run against a plugin the test subject could not see.
Filed as oleks/emmett#387 with both halves: install
claudecode-linterdeclaratively on emmett, and make the skip visible (aCCL ⏭token or a stderr notice) so a future PATH regression cannot silently disarm it again. That second half matters independently — it is the difference between a guard that is currently off and a guard that can turn itself off unnoticed.Keeping this issue open until emmett#387 lands, since the corruption remains unguarded across all 30 plugin repos in the workspace until then. Nothing further to do in this repo: the documentation fixes are in, and the mechanical fix is neither a new rule nor located here.
Both acceptance boxes met, and the reason not to close this is now gone.
The standing objection (recorded in the handoff) was: "Did not close #1 on the documentation fix. A docs-only close for a silent-corruption bug looks done and is not." That was right. It no longer applies — this is not a docs-only close.
☑ Any documented mdformat invocation includes
--with mdformat-frontmatterSwept the trees where such an invocation could circulate:
~/projects/claude-plugins/**(all file types)CLAUDE.md§conventions, which mandates both pluginscicd-insights/HANDOFF.md~/projects/servers/emmett/**~/.claude/CLAUDE.md,settings.jsonnixos/git-hooks/**That last row is the one that matters most and I checked it specifically: no automated hook runs mdformat. Had the pre-push or pre-commit formatter chain invoked it
-gfm-only, every push would have been silently corrupting frontmatter. It doesn't.The single remaining
mdformat-gfm-without-frontmatterstring in the tree is prose describing the hazard ("The well-known half of this is mdformat-gfm — without it, every GFM table…"), not an invocation.☑
skills/woodpecker-trigger-discipline/SKILL.mdfrontmatter intactVerified by byte-diff against the installed plugin copy at
~/.claude/plugins/cache/oleks-local/cicd-insights/1.4.6/: identical,---delimiters andname:/description:present. Independently confirmed live — the skill routes correctly on all three SC-003 phrasings (oleks/claude-plugin-cicd-insights#2).The part that actually closes this
A recurrence can no longer be silent. oleks/emmett#387 is deployed and verified, and the detector was proven to block, not merely report:
That test deliberately used frontmatter-stripped-but-otherwise-valid markdown (H1 first line, all lines < 80 chars), so markdownlint had nothing to say. The first attempt at the probe reproduced this issue's original discovery exactly — markdownlint
MD041with context"______…"firing first — which is luck, not a guard. The second attempt isolated CCL as the blocker.So the corruption is now caught by the check built for it, on all ~30 plugin repos, at push time. Closing.
Related follow-up: oleks/emmett#389 (the same silent-skip shape still applies to the other lint slots).