# Your Skill's Frontmatter Has a Typo. Nothing Will Ever Tell You

> Source: <https://dev.to/haoli/your-skills-frontmatter-has-a-typo-nothing-will-ever-tell-you-18b5>
> Published: 2026-10-11 09:28:41+00:00

A few days ago I shipped a Claude Code command with this frontmatter:

```
---
name: deploy-helper
description: "Helps with deployments"
effort: high
---
```

`effort: high` was supposed to keep the model from going overboard. I ran

`claude plugin validate` before shipping. It passed. Green. Ship it.

Except `effort` is not a Claude Code frontmatter key. It's a Codex concept. My

setting silently fell back to the session default, and the official validator

— the thing whose entire job is to validate — had nothing to say about it.

This is the failure mode that bothers me most: not the loud crash, but the

silent no-op. A misspelled `PreToolUs` hook never fires. A `licence` key never

licenses anything. You read your own config, it looks right, and it does

nothing. Forever.

So I built `frontmatter-guard`: a semantic linter for skill/plugin frontmatter

that knows the real key vocabulary and the real hook event list, and fails

loudly when you stray from it.

```
pip install frontmatter-guard
frontmatter-guard check .claude/ --strict
commands/deploy.md:4: error [unknown-key] Unknown key 'effort'.
    fix: Remove the key -- unknown keys are silently ignored, so it currently does nothing.
commands/deploy.md:7: error [unknown-hook-event] Unknown hook event 'PreToolUs'. Did you mean 'PreToolUse'?
    fix: Rename 'PreToolUs' to 'PreToolUse'. Unknown hook events never fire.
```

`effort`/` licence`/` descripton` catcher.`PreToolUse`, `PostToolUse`, `SessionStart`, `SessionEnd`, `UserPromptSubmit`, `Stop`, `SubagentStop`, `Notification`, `PreCompact`). A wrong event name means your hook never runs, which is exactly the kind of thing you want CI to scream about.`name`/` description` absent or empty.`version` that isn't semver-ish.`hooks:` as a string, `name:` as a number, that sort of thing.
Every finding carries `file:line`, the rule name, and a concrete fix suggestion. Exit codes are CI-ready: `1` on any error (or any warning under `--strict`), `0` when clean, `2` on usage errors. `--format json` for machines.

Because they answer different questions. `claude plugin validate` asks "will this plugin load?" — compatibility and structure. frontmatter-guard asks "does everything you wrote actually do something?" — semantic strictness. Unknown keys sail through the official check silently; they fail here. They're complementary, not competing.

There's also damson/skill-lint, a CI action doing structural checks on skills. frontmatter-guard is a local stdlib-only CLI doing semantic checks — same command works in pre-commit for instant feedback and in CI for enforcement.

Zero dependencies, Python 3.9+. The YAML parsing is a hand-rolled subset parser in the standard library — block maps and sequences, inline flow collections, literal blocks, quoted scalars. That sounds risky, but the design decision is deliberate: frontmatter is a small, boring corner of YAML, and a dependency-free parser means the tool installs in one second and runs anywhere, including locked-down CI runners. Anything outside the subset produces a `parse-warning` and the lint continues — a linter that crashes on weird input is worse than useless.

It scans both morphologies: `*.md` frontmatter behind `---` fences (skills, commands) and `plugin.json` files, with the same rule set applied to both.

```
pip install frontmatter-guard
frontmatter-guard check . --strict
```

GitHub: [https://github.com/hahahahahahahahah6/frontmatter-guard](https://github.com/hahahahahahahahah6/frontmatter-guard)

PyPI: [https://pypi.org/project/frontmatter-guard/](https://pypi.org/project/frontmatter-guard/)

If it catches a typo that would have silently shipped, that was the whole point.
