Repo: legioncodeinc/vibe-coding-tools
File: .cursor/skills/queen-bee-stinger/references/scripts/per-type-validation.py
Commit tested: eecb4acfc30500a5e88a2828471215a6cc5b1ef4 (eecb4ac)
SHA-256 of file tested: 010489135a877ffa14fe95a1e41c906d4338a80e4b940b82b1dd2238c1b078e7
Environment: Python 3.13 / macOS 15 (Darwin 25.6.0), PyYAML 6.0.3
Reported by: Main Street Mentor — Josh Collins josh.collins@yourmainstreetmentor.com
How these were found, and why you're getting them
Main Street Mentor vendors this file byte-identical into msm-skills and wraps it in a CI gate
(_tooling/check-skills.py) that validates every skill's frontmatter on each PR. Attribution and
licence are retained per §3.1 — see the repo's _tooling/skill-validator/NOTICE.md.
A CodeRabbit review of the PR that added that gate
(Main-Street-Mentor/msm-skills#81)
flagged these four in your file, all as 🟠 Major. Credit for finding them is CodeRabbit's; we
reproduced each one against eecb4ac before writing this up, and every transcript below is real
output, not a reconstruction.
We deliberately have not patched our vendored copy — keeping it byte-identical is what makes the
provenance checkable and keeps our next sync from you a clean re-copy instead of a manual merge. So
these are going to you rather than being forked away quietly. We've hardened our own wrapper to
refuse malformed validator output instead, which covers defects 3 and 4 (they crash, and a
crash can be detected from outside). It cannot cover 1 and 2, because those produce a
perfectly well-formed report that is simply wrong — those need a fix in this file.
Two clusters, in priority order.
Cluster A — silent false negatives (a wrapper cannot detect these)
These are the dangerous ones. The validator exits 0, emits a clean JSON report, and reports
zero findings on input that is genuinely invalid. Anyone using this as a CI gate gets a green
check that means nothing. No amount of output validation on the caller's side can see a check that
was never run.
1. A YAML parse failure is silently downgraded to the fallback parser
Location: parse_frontmatter(), ~L204–213
if HAVE_YAML:
try:
data = yaml.safe_load(fm_text)
return data if isinstance(data, dict) else {}
except Exception:
return parse_simple_yaml(fm_text)
When yaml.safe_load() raises, the bare except Exception falls through to parse_simple_yaml(),
whose docstring is explicit that it does not support nested maps, multi-line scalars, anchors, or
tags. It does not fail on syntax it cannot represent — it guesses. So malformed YAML becomes a
plausible-looking dict and the parse error is never reported.
parse_simple_yaml() is documented as the fallback for "when PyYAML is unavailable". Using it as
an error handler when PyYAML is available is a different thing, and it converts a hard parse
error into a silent misread.
Reproduce
$ cat d1/SKILL.md
---
name: d1
description: [unterminated
---
body
$ python3 per-type-validation.py --type skill --harness cowork --json d1
{
"exit_code": 0,
"findings": [],
"path": "d1",
"summary": { "errors": 0, "infos": 0, "warnings": 0 }
}
$ echo $?
0
yaml.safe_load raises on that frontmatter. The fallback sees rest.startswith("[") but not
endswith("]"), so it stores the literal string "[unterminated" — a 13-character description that
passes every length check. The file is not loadable by any harness that uses a real YAML parser, and
the validator calls it clean.
Suggested fix: report the parse error as an ERROR finding. Keep parse_simple_yaml() for the
genuine no-PyYAML case, and when it is the parser, reject syntax outside its documented subset
rather than guessing at it.
2. Non-string name / description skip every check silently
Location: validate_skill(), ~L266–296
Each check is guarded by isinstance(..., str):
elif isinstance(description, str): # L273 — both Cowork length checks
if isinstance(name, str) and not is_placeholder(name) and "cowork" in harnesses: # L283
if isinstance(name, str) and not is_placeholder(name) and "cursor" in harnesses: # L293
With PyYAML present, name: [x] parses to a list, every guard is False, and all name
validation — length, kebab-case, reserved words, Cursor folder-name match — plus both description
length ceilings are skipped without a word. The guards are correct as crash-prevention; the problem
is that there is no else.
Reproduce — the same semantic content, differing only by brackets:
$ cat d2/SKILL.md
---
name: [Claude_BAD_NAME_THAT_IS_NOT_KEBAB]
description: [x]
---
$ python3 per-type-validation.py --type skill --harness cowork --json d2
{ "exit_code": 0, "findings": [], "summary": { "errors": 0, "infos": 0, "warnings": 0 } }
$ cat d2b/SKILL.md # identical values, unbracketed
---
name: Claude_BAD_NAME_THAT_IS_NOT_KEBAB
description: x
---
$ python3 per-type-validation.py --type skill --harness cowork --json d2b
{
"exit_code": 1,
"findings": [
{ "severity": "ERROR", "label": "SKILL.md",
"message": "name 'Claude_BAD_NAME_THAT_IS_NOT_KEBAB' is not kebab-case" },
{ "severity": "ERROR", "label": "SKILL.md",
"message": "name 'Claude_BAD_NAME_THAT_IS_NOT_KEBAB' contains reserved word 'claude'" }
],
"summary": { "errors": 2, "infos": 0, "warnings": 0 }
}
Two ERRORs and exit 1 become zero findings and exit 0, from adding brackets.
Suggested fix: before the length/format/reserved-word checks, emit an ERROR when name or
description is present but not a string. That closes the false negative and, as a bonus, removes
the crash risk if one of those isinstance guards is ever dropped during a refactor.
Cluster B — unhandled exceptions escape main()
main() catches only OSError (~L633). Any TypeError / AttributeError from a validator function
therefore propagates out, so the process dies with a traceback, prints nothing on stdout, and
exits 1 — which is also the validator's normal "found ERROR findings" exit code. Under --json
that means a caller distinguishing "clean" from "found errors" by exit code alone sees a normal
result for a run that never happened.
3. --type agent: list-valued color / memory / isolation raise TypeError
Location: validate_agent(), L372 / L376 / L380
if color is not None and color not in AGENT_COLOR_ENUM:
AGENT_COLOR_ENUM is a set, so an unhashable value raises before the membership test resolves.
Reproduce
$ cat d3/d3.md
---
name: d3
description: x
color: [red]
---
$ python3 per-type-validation.py --type agent --harness claude-code --json d3/d3.md
Traceback (most recent call last):
File "per-type-validation.py", line 648, in <module>
sys.exit(main())
File "per-type-validation.py", line 624, in main
validate_agent(root, harnesses, args.target, report)
File "per-type-validation.py", line 372, in validate_agent
if color is not None and color not in AGENT_COLOR_ENUM:
TypeError: cannot use 'list' as a set element (unhashable type: 'list')
$ echo $?
1
$ python3 per-type-validation.py --type agent --harness claude-code --json d3/d3.md 2>/dev/null | wc -c
0 # exit 1 with EMPTY stdout under --json
The same section has a false negative alongside the crash: a non-string name skips the agent name
checks entirely, same shape as defect 2.
Suggested fix: reject non-string color / memory / isolation / name with an ERROR before
the enum and regex tests. Independently, widening main()'s except OSError to also catch
Exception and emit a structured error report would make every remaining case of this class
survivable for JSON consumers.
4. --type plugin: a non-object manifest, or a non-string name, raises
Location: validate_plugin(), ~L490–501
data = json.loads(text) # not checked for being a dict
...
name = data.get("name") # L495
...
elif not PLUGIN_NAME_RE.match(name): # L500
Reproduce
$ echo '[]' > d4a/.claude-plugin/plugin.json
$ python3 per-type-validation.py --type plugin --harness cowork --json d4a
File "per-type-validation.py", line 495, in validate_plugin
name = data.get("name")
AttributeError: 'list' object has no attribute 'get'
$ echo $? -> 1, stdout empty
$ echo '{"name": 1}' > d4b/.claude-plugin/plugin.json
$ python3 per-type-validation.py --type plugin --harness cowork --json d4b
File "per-type-validation.py", line 500, in validate_plugin
elif not PLUGIN_NAME_RE.match(name):
TypeError: expected string or bytes-like object, got 'int'
$ echo $? -> 1, stdout empty
Both are plausible authoring mistakes (a JSON array at the top level, a version-like value left
unquoted), and both are exactly the case a plugin validator exists to catch.
Suggested fix: after json.loads, ERROR and return if the result is not a dict; ERROR if
name is present but not a string, before the placeholder and regex checks.
Summary
| # |
Type |
Location |
Symptom |
Detectable by a caller? |
| 1 |
False negative |
parse_frontmatter L204–213 |
Invalid YAML reported clean, exit 0 |
No |
| 2 |
False negative |
validate_skill L266–296 |
Non-string name/description skips all checks, exit 0 |
No |
| 3 |
Crash |
validate_agent L372/376/380 |
TypeError, empty stdout, exit 1 |
Yes |
| 4 |
Crash |
validate_plugin L495/500 |
AttributeError/TypeError, empty stdout, exit 1 |
Yes |
The two clusters have different urgency. Cluster B is loud — anyone wrapping this script can detect
it from outside, and we do. Cluster A is silent, and a silent false negative in a validation gate is
worse than a crash: a crash stops the line, a false pass ships.
Happy to open a PR against vibe-coding-tools with fixes for all four if that's useful — say the
word and we'll shape it to your conventions rather than guess at them. The script is genuinely good
and it's doing real work for us; this is a bug report, not a complaint.
Repo:
legioncodeinc/vibe-coding-toolsFile:
.cursor/skills/queen-bee-stinger/references/scripts/per-type-validation.pyCommit tested:
eecb4acfc30500a5e88a2828471215a6cc5b1ef4(eecb4ac)SHA-256 of file tested:
010489135a877ffa14fe95a1e41c906d4338a80e4b940b82b1dd2238c1b078e7Environment: Python 3.13 / macOS 15 (Darwin 25.6.0), PyYAML 6.0.3
Reported by: Main Street Mentor — Josh Collins josh.collins@yourmainstreetmentor.com
How these were found, and why you're getting them
Main Street Mentor vendors this file byte-identical into
msm-skillsand wraps it in a CI gate(
_tooling/check-skills.py) that validates every skill's frontmatter on each PR. Attribution andlicence are retained per §3.1 — see the repo's
_tooling/skill-validator/NOTICE.md.A CodeRabbit review of the PR that added that gate
(Main-Street-Mentor/msm-skills#81)
flagged these four in your file, all as 🟠 Major. Credit for finding them is CodeRabbit's; we
reproduced each one against
eecb4acbefore writing this up, and every transcript below is realoutput, not a reconstruction.
We deliberately have not patched our vendored copy — keeping it byte-identical is what makes the
provenance checkable and keeps our next sync from you a clean re-copy instead of a manual merge. So
these are going to you rather than being forked away quietly. We've hardened our own wrapper to
refuse malformed validator output instead, which covers defects 3 and 4 (they crash, and a
crash can be detected from outside). It cannot cover 1 and 2, because those produce a
perfectly well-formed report that is simply wrong — those need a fix in this file.
Two clusters, in priority order.
Cluster A — silent false negatives (a wrapper cannot detect these)
These are the dangerous ones. The validator exits 0, emits a clean JSON report, and reports
zero findings on input that is genuinely invalid. Anyone using this as a CI gate gets a green
check that means nothing. No amount of output validation on the caller's side can see a check that
was never run.
1. A YAML parse failure is silently downgraded to the fallback parser
Location:
parse_frontmatter(), ~L204–213When
yaml.safe_load()raises, the bareexcept Exceptionfalls through toparse_simple_yaml(),whose docstring is explicit that it does not support nested maps, multi-line scalars, anchors, or
tags. It does not fail on syntax it cannot represent — it guesses. So malformed YAML becomes a
plausible-looking dict and the parse error is never reported.
parse_simple_yaml()is documented as the fallback for "when PyYAML is unavailable". Using it asan error handler when PyYAML is available is a different thing, and it converts a hard parse
error into a silent misread.
Reproduce
yaml.safe_loadraises on that frontmatter. The fallback seesrest.startswith("[")but notendswith("]"), so it stores the literal string"[unterminated"— a 13-character description thatpasses every length check. The file is not loadable by any harness that uses a real YAML parser, and
the validator calls it clean.
Suggested fix: report the parse error as an
ERRORfinding. Keepparse_simple_yaml()for thegenuine no-PyYAML case, and when it is the parser, reject syntax outside its documented subset
rather than guessing at it.
2. Non-string
name/descriptionskip every check silentlyLocation:
validate_skill(), ~L266–296Each check is guarded by
isinstance(..., str):With PyYAML present,
name: [x]parses to alist, every guard isFalse, and all namevalidation — length, kebab-case, reserved words, Cursor folder-name match — plus both description
length ceilings are skipped without a word. The guards are correct as crash-prevention; the problem
is that there is no
else.Reproduce — the same semantic content, differing only by brackets:
Two ERRORs and exit 1 become zero findings and exit 0, from adding brackets.
Suggested fix: before the length/format/reserved-word checks, emit an
ERRORwhennameordescriptionis present but not a string. That closes the false negative and, as a bonus, removesthe crash risk if one of those
isinstanceguards is ever dropped during a refactor.Cluster B — unhandled exceptions escape
main()main()catches onlyOSError(~L633). AnyTypeError/AttributeErrorfrom a validator functiontherefore propagates out, so the process dies with a traceback, prints nothing on stdout, and
exits 1 — which is also the validator's normal "found ERROR findings" exit code. Under
--jsonthat means a caller distinguishing "clean" from "found errors" by exit code alone sees a normal
result for a run that never happened.
3.
--type agent: list-valuedcolor/memory/isolationraiseTypeErrorLocation:
validate_agent(), L372 / L376 / L380AGENT_COLOR_ENUMis aset, so an unhashable value raises before the membership test resolves.Reproduce
The same section has a false negative alongside the crash: a non-string
nameskips the agent namechecks entirely, same shape as defect 2.
Suggested fix: reject non-string
color/memory/isolation/namewith anERRORbeforethe enum and regex tests. Independently, widening
main()'sexcept OSErrorto also catchExceptionand emit a structured error report would make every remaining case of this classsurvivable for JSON consumers.
4.
--type plugin: a non-object manifest, or a non-stringname, raisesLocation:
validate_plugin(), ~L490–501Reproduce
Both are plausible authoring mistakes (a JSON array at the top level, a version-like value left
unquoted), and both are exactly the case a plugin validator exists to catch.
Suggested fix: after
json.loads,ERRORand return if the result is not adict;ERRORifnameis present but not a string, before the placeholder and regex checks.Summary
parse_frontmatterL204–213validate_skillL266–296name/descriptionskips all checks, exit 0validate_agentL372/376/380TypeError, empty stdout, exit 1validate_pluginL495/500AttributeError/TypeError, empty stdout, exit 1The two clusters have different urgency. Cluster B is loud — anyone wrapping this script can detect
it from outside, and we do. Cluster A is silent, and a silent false negative in a validation gate is
worse than a crash: a crash stops the line, a false pass ships.
Happy to open a PR against
vibe-coding-toolswith fixes for all four if that's useful — say theword and we'll shape it to your conventions rather than guess at them. The script is genuinely good
and it's doing real work for us; this is a bug report, not a complaint.