frontmatter-guard 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hao li
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: frontmatter-guard
3
+ Version: 0.1.0
4
+ Summary: Semantic linter for Claude Code skill/plugin frontmatter — catches silent key typos official validators miss
5
+ Author: hao li
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/hahahahahahahahah6/frontmatter-guard
8
+ Keywords: claude-code,frontmatter,linter,skills,plugins,developer-tools
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Dynamic: license-file
13
+
14
+ # frontmatter-guard
15
+
16
+ A semantic linter for Claude Code skill/plugin frontmatter. Catches the silent
17
+ mistakes the official validator waves through.
18
+
19
+ ## The problem
20
+
21
+ `claude plugin validate` checks that your plugin is structurally compatible.
22
+ It does **not** check that your frontmatter keys actually mean anything. I
23
+ found this out the embarrassing way: I had written `effort: high` in a command
24
+ frontmatter, expecting it to constrain the model. `claude plugin validate`
25
+ passed it without a word. In reality `effort` is not a Claude Code frontmatter
26
+ key at all — it is a Codex concept — so my setting silently fell back to the
27
+ session default and I had no idea.
28
+
29
+ Misspelled keys and misspelled hook events fail the same silent way:
30
+ `PreToolUs` never fires, `licence` never licenses anything, and nothing ever
31
+ tells you.
32
+
33
+ frontmatter-guard closes that gap: it knows the real key vocabulary and the
34
+ real hook event list, and it tells you — with `file:line`, a rule name, and a
35
+ fix suggestion — when something you wrote does nothing.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install frontmatter-guard
41
+ ```
42
+
43
+ Zero dependencies. Python 3.9+. Works anywhere, including pre-commit and CI.
44
+
45
+ ## Quickstart
46
+
47
+ ```bash
48
+ # lint one file or a whole directory
49
+ frontmatter-guard check SKILL.md
50
+ frontmatter-guard check .claude/ --strict
51
+
52
+ # machine-readable output for CI
53
+ frontmatter-guard check . --format json
54
+ ```
55
+
56
+ Example output:
57
+
58
+ ```
59
+ commands/deploy.md:4: error [unknown-key] Unknown key 'effort'.
60
+ fix: Remove the key -- unknown keys are silently ignored, so it currently does nothing.
61
+ commands/deploy.md:5: error [unknown-key] Unknown key 'licence'. Did you mean 'license'?
62
+ fix: Rename 'licence' to 'license'. Unknown keys are silently ignored, so fix the typo or remove the key.
63
+ commands/deploy.md:7: error [unknown-hook-event] Unknown hook event 'PreToolUs'. Did you mean 'PreToolUse'?
64
+ fix: Rename 'PreToolUs' to 'PreToolUse'. Unknown hook events never fire.
65
+ frontmatter-guard: 3 error(s), 0 warning(s) in 1 file(s)
66
+ ```
67
+
68
+ Exit codes are CI-ready: exit `1` when any error-level finding exists (or any
69
+ warning under `--strict`); exit `0` when clean; exit `2` on usage errors.
70
+
71
+ ## Rules
72
+
73
+ | Rule | Severity | What it catches |
74
+ |------|----------|-----------------|
75
+ | `unknown-key` | error | Top-level key not in the known skill/plugin vocabulary. Suggests the closest real key via difflib ("Did you mean 'license'?"). |
76
+ | `unknown-hook-event` | error | Hook event name not in Claude Code's event list (`PreToolUse`, `PostToolUse`, `SessionStart`, …). Unknown events never fire. |
77
+ | `missing-required` | error | `name` or `description` absent or empty. |
78
+ | `bad-version` | warning | `version` that isn't semver-ish (`1.2.3`, `v0.1`, `2.0.0-beta`). |
79
+ | `bad-type` | warning | Value of the wrong type (`hooks:` as a string, `name:` as a number, …). |
80
+ | `parse-warning` | warning | Frontmatter uses YAML beyond the supported subset — the file is still linted, not crashed. |
81
+
82
+ Scans `*.md` frontmatter (`---` fences) and `plugin.json` files. The built-in
83
+ YAML parser is stdlib-only and covers the shapes frontmatter actually uses:
84
+ scalars, block maps/sequences, inline flow collections, literal blocks.
85
+ Anything fancier gets a warning, never an exception.
86
+
87
+ ## How it differs
88
+
89
+ - **vs `claude plugin validate` (official):** the official validator checks
90
+ compatibility and structure — "will this plugin load?" frontmatter-guard
91
+ checks semantic strictness — "does everything you wrote actually do
92
+ something?" Unknown keys sail through the official check silently; they
93
+ fail here.
94
+ - **vs damson/skill-lint:** that project is a CI action doing structural
95
+ checks on skills. frontmatter-guard is a local, stdlib-only CLI doing
96
+ *semantic* checks (typo suggestions, event-name validation, type/format
97
+ checks). Use it in pre-commit for instant feedback *and* in CI for
98
+ enforcement — same command, both places.
99
+
100
+ ## CI integration
101
+
102
+ GitHub Actions:
103
+
104
+ ```yaml
105
+ - name: Lint skill/plugin frontmatter
106
+ run: |
107
+ pip install frontmatter-guard
108
+ frontmatter-guard check . --strict
109
+ ```
110
+
111
+ pre-commit:
112
+
113
+ ```yaml
114
+ repos:
115
+ - repo: local
116
+ hooks:
117
+ - id: frontmatter-guard
118
+ name: frontmatter-guard
119
+ entry: frontmatter-guard check
120
+ language: system
121
+ types: [markdown]
122
+ args: [--strict]
123
+ ```
124
+
125
+ ## Development
126
+
127
+ ```bash
128
+ python3 -m unittest discover -s tests # 34 tests, stdlib only
129
+ ```
130
+
131
+ ## License
132
+
133
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,120 @@
1
+ # frontmatter-guard
2
+
3
+ A semantic linter for Claude Code skill/plugin frontmatter. Catches the silent
4
+ mistakes the official validator waves through.
5
+
6
+ ## The problem
7
+
8
+ `claude plugin validate` checks that your plugin is structurally compatible.
9
+ It does **not** check that your frontmatter keys actually mean anything. I
10
+ found this out the embarrassing way: I had written `effort: high` in a command
11
+ frontmatter, expecting it to constrain the model. `claude plugin validate`
12
+ passed it without a word. In reality `effort` is not a Claude Code frontmatter
13
+ key at all — it is a Codex concept — so my setting silently fell back to the
14
+ session default and I had no idea.
15
+
16
+ Misspelled keys and misspelled hook events fail the same silent way:
17
+ `PreToolUs` never fires, `licence` never licenses anything, and nothing ever
18
+ tells you.
19
+
20
+ frontmatter-guard closes that gap: it knows the real key vocabulary and the
21
+ real hook event list, and it tells you — with `file:line`, a rule name, and a
22
+ fix suggestion — when something you wrote does nothing.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install frontmatter-guard
28
+ ```
29
+
30
+ Zero dependencies. Python 3.9+. Works anywhere, including pre-commit and CI.
31
+
32
+ ## Quickstart
33
+
34
+ ```bash
35
+ # lint one file or a whole directory
36
+ frontmatter-guard check SKILL.md
37
+ frontmatter-guard check .claude/ --strict
38
+
39
+ # machine-readable output for CI
40
+ frontmatter-guard check . --format json
41
+ ```
42
+
43
+ Example output:
44
+
45
+ ```
46
+ commands/deploy.md:4: error [unknown-key] Unknown key 'effort'.
47
+ fix: Remove the key -- unknown keys are silently ignored, so it currently does nothing.
48
+ commands/deploy.md:5: error [unknown-key] Unknown key 'licence'. Did you mean 'license'?
49
+ fix: Rename 'licence' to 'license'. Unknown keys are silently ignored, so fix the typo or remove the key.
50
+ commands/deploy.md:7: error [unknown-hook-event] Unknown hook event 'PreToolUs'. Did you mean 'PreToolUse'?
51
+ fix: Rename 'PreToolUs' to 'PreToolUse'. Unknown hook events never fire.
52
+ frontmatter-guard: 3 error(s), 0 warning(s) in 1 file(s)
53
+ ```
54
+
55
+ Exit codes are CI-ready: exit `1` when any error-level finding exists (or any
56
+ warning under `--strict`); exit `0` when clean; exit `2` on usage errors.
57
+
58
+ ## Rules
59
+
60
+ | Rule | Severity | What it catches |
61
+ |------|----------|-----------------|
62
+ | `unknown-key` | error | Top-level key not in the known skill/plugin vocabulary. Suggests the closest real key via difflib ("Did you mean 'license'?"). |
63
+ | `unknown-hook-event` | error | Hook event name not in Claude Code's event list (`PreToolUse`, `PostToolUse`, `SessionStart`, …). Unknown events never fire. |
64
+ | `missing-required` | error | `name` or `description` absent or empty. |
65
+ | `bad-version` | warning | `version` that isn't semver-ish (`1.2.3`, `v0.1`, `2.0.0-beta`). |
66
+ | `bad-type` | warning | Value of the wrong type (`hooks:` as a string, `name:` as a number, …). |
67
+ | `parse-warning` | warning | Frontmatter uses YAML beyond the supported subset — the file is still linted, not crashed. |
68
+
69
+ Scans `*.md` frontmatter (`---` fences) and `plugin.json` files. The built-in
70
+ YAML parser is stdlib-only and covers the shapes frontmatter actually uses:
71
+ scalars, block maps/sequences, inline flow collections, literal blocks.
72
+ Anything fancier gets a warning, never an exception.
73
+
74
+ ## How it differs
75
+
76
+ - **vs `claude plugin validate` (official):** the official validator checks
77
+ compatibility and structure — "will this plugin load?" frontmatter-guard
78
+ checks semantic strictness — "does everything you wrote actually do
79
+ something?" Unknown keys sail through the official check silently; they
80
+ fail here.
81
+ - **vs damson/skill-lint:** that project is a CI action doing structural
82
+ checks on skills. frontmatter-guard is a local, stdlib-only CLI doing
83
+ *semantic* checks (typo suggestions, event-name validation, type/format
84
+ checks). Use it in pre-commit for instant feedback *and* in CI for
85
+ enforcement — same command, both places.
86
+
87
+ ## CI integration
88
+
89
+ GitHub Actions:
90
+
91
+ ```yaml
92
+ - name: Lint skill/plugin frontmatter
93
+ run: |
94
+ pip install frontmatter-guard
95
+ frontmatter-guard check . --strict
96
+ ```
97
+
98
+ pre-commit:
99
+
100
+ ```yaml
101
+ repos:
102
+ - repo: local
103
+ hooks:
104
+ - id: frontmatter-guard
105
+ name: frontmatter-guard
106
+ entry: frontmatter-guard check
107
+ language: system
108
+ types: [markdown]
109
+ args: [--strict]
110
+ ```
111
+
112
+ ## Development
113
+
114
+ ```bash
115
+ python3 -m unittest discover -s tests # 34 tests, stdlib only
116
+ ```
117
+
118
+ ## License
119
+
120
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "frontmatter-guard"
7
+ version = "0.1.0"
8
+ description = "Semantic linter for Claude Code skill/plugin frontmatter — catches silent key typos official validators miss"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "hao li" }]
13
+ keywords = ["claude-code", "frontmatter", "linter", "skills", "plugins", "developer-tools"]
14
+
15
+ [project.scripts]
16
+ frontmatter-guard = "frontmatter_guard.cli:main"
17
+
18
+ [project.urls]
19
+ Homepage = "https://github.com/hahahahahahahahah6/frontmatter-guard"
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """frontmatter-guard: semantic linter for Claude Code skill/plugin frontmatter."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .schema import lint_frontmatter # noqa: F401
@@ -0,0 +1,5 @@
1
+ """Allow `python -m frontmatter_guard`."""
2
+ from .cli import main
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -0,0 +1,201 @@
1
+ """Command-line interface for frontmatter-guard."""
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+
8
+ from . import __version__
9
+ from .schema import (
10
+ SEVERITY_ERROR,
11
+ SEVERITY_WARNING,
12
+ extract_md_frontmatter,
13
+ lint_frontmatter,
14
+ )
15
+
16
+ SCAN_EXTENSIONS = (".md",)
17
+ SCAN_FILENAMES = ("plugin.json",)
18
+
19
+
20
+ def iter_target_files(path):
21
+ """Yield (path) for every scannable file under path."""
22
+ if os.path.isfile(path):
23
+ yield path
24
+ return
25
+ for root, dirs, files in os.walk(path):
26
+ dirs[:] = [d for d in dirs
27
+ if d not in (".git", "__pycache__", "node_modules",
28
+ ".venv", "venv", "dist", "build")]
29
+ for name in sorted(files):
30
+ full = os.path.join(root, name)
31
+ if name.endswith(SCAN_EXTENSIONS) or name in SCAN_FILENAMES:
32
+ yield full
33
+
34
+
35
+ def lint_file(path):
36
+ """Lint one file. Returns a list of finding dicts."""
37
+ try:
38
+ with open(path, "r", encoding="utf-8") as handle:
39
+ text = handle.read()
40
+ except (OSError, UnicodeDecodeError) as exc:
41
+ return [{
42
+ "file": path,
43
+ "line": 1,
44
+ "rule": "unreadable",
45
+ "severity": SEVERITY_ERROR,
46
+ "message": "Could not read file: %s" % exc,
47
+ "fix": "Check file permissions and encoding (UTF-8 expected).",
48
+ }]
49
+
50
+ findings = []
51
+ name = os.path.basename(path)
52
+
53
+ if name.endswith(".md"):
54
+ extracted = extract_md_frontmatter(text)
55
+ if extracted is None:
56
+ if name == "SKILL.md":
57
+ findings.append({
58
+ "file": path,
59
+ "line": 1,
60
+ "rule": "no-frontmatter",
61
+ "severity": SEVERITY_WARNING,
62
+ "message": "SKILL.md has no frontmatter block.",
63
+ "fix": "Add a --- fenced frontmatter block with at "
64
+ "least name and description.",
65
+ })
66
+ return findings
67
+ data, key_lines, start_line, parse_warnings = extracted
68
+ for warning in parse_warnings:
69
+ findings.append({
70
+ "file": path,
71
+ "line": start_line,
72
+ "rule": "parse-warning",
73
+ "severity": SEVERITY_WARNING,
74
+ "message": "Frontmatter YAML: %s." % warning,
75
+ "fix": "Simplify the frontmatter to plain key: value pairs, "
76
+ "lists and nested maps.",
77
+ })
78
+ findings.extend(lint_frontmatter(data, key_lines, path, start_line))
79
+ return findings
80
+
81
+ # plugin.json and friends: JSON object, same key vocabulary
82
+ try:
83
+ data = json.loads(text)
84
+ except ValueError as exc:
85
+ return [{
86
+ "file": path,
87
+ "line": 1,
88
+ "rule": "parse-warning",
89
+ "severity": SEVERITY_ERROR,
90
+ "message": "Invalid JSON: %s" % exc,
91
+ "fix": "Fix the JSON syntax.",
92
+ }]
93
+ if not isinstance(data, dict):
94
+ return [{
95
+ "file": path,
96
+ "line": 1,
97
+ "rule": "bad-type",
98
+ "severity": SEVERITY_ERROR,
99
+ "message": "Top-level JSON value must be an object.",
100
+ "fix": "Make the top level a JSON object with plugin keys.",
101
+ }]
102
+ key_lines = {key: 1 for key in data}
103
+ return lint_frontmatter(data, key_lines, path, start_line=1)
104
+
105
+
106
+ def check_targets(targets):
107
+ findings = []
108
+ files = []
109
+ for target in targets:
110
+ if not os.path.exists(target):
111
+ return None, ["no such file or directory: %s" % target]
112
+ for path in iter_target_files(target):
113
+ files.append(path)
114
+ findings.extend(lint_file(path))
115
+ return (files, findings), []
116
+
117
+
118
+ def format_text(files, findings):
119
+ out = []
120
+ for finding in findings:
121
+ out.append("%s:%s: %s [%s] %s"
122
+ % (finding["file"], finding["line"],
123
+ finding["severity"], finding["rule"],
124
+ finding["message"]))
125
+ if finding.get("fix"):
126
+ out.append(" fix: %s" % finding["fix"])
127
+ errors = sum(1 for f in findings if f["severity"] == SEVERITY_ERROR)
128
+ warnings = sum(1 for f in findings if f["severity"] == SEVERITY_WARNING)
129
+ out.append("frontmatter-guard: %d error(s), %d warning(s) in %d file(s)"
130
+ % (errors, warnings, len(files)))
131
+ return "\n".join(out)
132
+
133
+
134
+ def format_json(files, findings):
135
+ errors = sum(1 for f in findings if f["severity"] == SEVERITY_ERROR)
136
+ warnings = sum(1 for f in findings if f["severity"] == SEVERITY_WARNING)
137
+ return json.dumps({
138
+ "tool": "frontmatter-guard",
139
+ "version": __version__,
140
+ "files": files,
141
+ "findings": findings,
142
+ "summary": {
143
+ "files": len(files),
144
+ "errors": errors,
145
+ "warnings": warnings,
146
+ },
147
+ }, indent=2)
148
+
149
+
150
+ def build_parser():
151
+ parser = argparse.ArgumentParser(
152
+ prog="frontmatter-guard",
153
+ description="Semantic linter for Claude Code skill/plugin "
154
+ "frontmatter.")
155
+ parser.add_argument("--version", action="version",
156
+ version="frontmatter-guard %s" % __version__)
157
+ sub = parser.add_subparsers(dest="command")
158
+ check = sub.add_parser("check", help="Lint frontmatter in a file or "
159
+ "directory.")
160
+ check.add_argument("targets", nargs="+",
161
+ help="File(s) or directorie(s) to scan.")
162
+ check.add_argument("--format", choices=("text", "json"), default="text",
163
+ help="Output format (default: text).")
164
+ check.add_argument("--strict", action="store_true",
165
+ help="Treat warnings as failures (exit 1).")
166
+ return parser
167
+
168
+
169
+ def main(argv=None):
170
+ parser = build_parser()
171
+ args = parser.parse_args(argv)
172
+
173
+ if args.command != "check":
174
+ parser.print_help()
175
+ return 2
176
+
177
+ result, errors = check_targets(args.targets)
178
+ if result is None:
179
+ for error in errors:
180
+ print("frontmatter-guard: error: %s" % error, file=sys.stderr)
181
+ return 2
182
+ files, findings = result
183
+
184
+ if args.format == "json":
185
+ print(format_json(files, findings))
186
+ else:
187
+ print(format_text(files, findings))
188
+
189
+ error_count = sum(1 for f in findings
190
+ if f["severity"] == SEVERITY_ERROR)
191
+ warning_count = sum(1 for f in findings
192
+ if f["severity"] == SEVERITY_WARNING)
193
+ if error_count > 0:
194
+ return 1
195
+ if args.strict and warning_count > 0:
196
+ return 1
197
+ return 0
198
+
199
+
200
+ if __name__ == "__main__":
201
+ sys.exit(main())