Skip to content

Add YAML validation for SKILL.md frontmatter and fix JSON check - #9

Merged
GentBajko merged 1 commit into
mainfrom
fix/skill-frontmatter-yaml
Aug 30, 2026
Merged

Add YAML validation for SKILL.md frontmatter and fix JSON check#9
GentBajko merged 1 commit into
mainfrom
fix/skill-frontmatter-yaml

Conversation

@GentBajko

@GentBajko GentBajko commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Summary

This PR improves the lint-sync validation script by adding YAML frontmatter validation for SKILL.md files and fixing a false positive in JSON validation for deleted files.

Key Changes

  • JSON validation fix: Skip JSON syntax checks for tracked files that have been deleted from the worktree, preventing false "invalid JSON" errors for pending deletions
  • YAML frontmatter validation: Add new check (12m) to validate SKILL.md frontmatter is valid YAML, using ruby (preferred), python3's yaml module (fallback), or a targeted regex scan for common YAML traps
  • Description fix: Update skills/map/SKILL.md to use an em dash instead of a colon in the description to avoid YAML parsing issues with plain scalars containing ": "
  • Cleanup: Remove .claude/settings.json configuration file

Implementation Details

The YAML validation check extracts frontmatter between --- delimiters and validates it using available tools in order of preference:

  1. Ruby with YAML.safe_load (macOS ships with psych)
  2. Python3 with yaml.safe_load
  3. Regex pattern matching for the specific trap of plain scalars containing ": "

This addresses a real issue where invalid frontmatter YAML would cause gh skill install to fail with an empty directory left behind, while existing grep-based checks would still pass.

https://claude.ai/code/session_01BPKdCvDCZzUuTmNTXRhRH6

Summary by Sourcery

Strengthen skill metadata validation by checking YAML frontmatter and handling deleted JSON files correctly.

New Features:

  • Add linting to validate YAML frontmatter in every SKILL.md file using available YAML parsers or a fallback check.

Bug Fixes:

  • Avoid reporting invalid JSON for tracked files that have been deleted from the worktree.
  • Correct map skill frontmatter so its description parses as valid YAML.

Chores:

  • Remove the obsolete .claude/settings.json configuration file.

`gh skill install` refused the map skill outright on main:

    failed to install skill "map": could not inject metadata:
    invalid frontmatter YAML: yaml: line 2: mapping values are not
    allowed in this context

and left an empty ~/.copilot/skills/map/ behind, so `gh skill list`
reported a skill with no content.

The cause is one character. A plain (unquoted) YAML scalar cannot
contain ": " - the parser reads it as a nested mapping - and map's
description had one buried mid-sentence: "One verb for both: no
reference yet means build it". Now an em-dash. Every other skill
passes because none of them happen to contain a colon-space.

This shipped: the flagship command could not be installed via the
GitHub CLI at all, on the release that introduced it.

Nothing here could have caught it. Check 3 reads `name:` out of the
frontmatter with sed and never asks whether the block is YAML, and
sed does not care. Check 12m now parses every SKILL.md frontmatter
with a real YAML parser (ruby's psych, python's yaml as fallback, a
targeted ": " scan if neither is present) and asserts name and
description survive the parse. Negative-tested by reintroducing the
colon.

Also, a smaller misdiagnosis in check 9: `git ls-files` still lists a
tracked file deleted from the worktree, so a pending deletion was
reported as "invalid JSON: <path>", sending the reader hunting for a
stray comma in a file that is not there. Missing files are now
skipped.

Drops .claude/settings.json, which only enabled the capstone plugin
for this project and follows the plugin having been uninstalled.
@sourcery-ai

sourcery-ai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR strengthens lint-sync by adding SKILL.md frontmatter YAML validation with Ruby/Python/regex fallbacks, avoids false JSON errors for tracked deletions, and fixes the map skill metadata plus obsolete Claude configuration.

Flow diagram for lint-sync validation checks

flowchart TD
    Start["lint-sync.sh"] --> JSON["Validate tracked JSON files"]
    JSON --> Exists{"File exists in worktree?"}
    Exists -->|No| Skip["Skip pending deletion"]
    Exists -->|Yes| ParseJSON["python3 json.load"]
    Skip --> Frontmatter["Validate SKILL.md frontmatter"]
    ParseJSON --> Frontmatter
    Frontmatter --> Ruby{"ruby available?"}
    Ruby -->|Yes| RubyYAML["YAML.safe_load"]
    Ruby -->|No| PyYAML{"python3 yaml available?"}
    PyYAML -->|Yes| PythonYAML["yaml.safe_load"]
    PyYAML -->|No| Regex["Scan for plain scalar : trap"]
    RubyYAML --> Result["Report validation error if invalid"]
    PythonYAML --> Result
    Regex --> Result
Loading

File-Level Changes

Change Details Files
Correct JSON validation to ignore tracked files that are absent because they are pending deletions.
  • Check file existence before attempting Python JSON parsing.
  • Preserve syntax validation for tracked JSON files still present in the worktree.
skills/core/scripts/lint-sync.sh
Add portable validation of SKILL.md YAML frontmatter to the synchronization lint script.
  • Extract frontmatter between exact YAML delimiter lines.
  • Prefer Ruby YAML.safe_load, then Python yaml.safe_load, with a targeted regex fallback.
  • Report invalid frontmatter through the existing lint error mechanism.
skills/core/scripts/lint-sync.sh
Resolve a frontmatter parsing failure in the map skill description.
  • Replace the problematic colon-containing separator with an em dash in the description plain scalar.
skills/map/SKILL.md
Remove obsolete Claude configuration.
  • Delete the repository-local Claude settings file.
.claude/settings.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@GentBajko
GentBajko merged commit 322e5c3 into main Aug 30, 2026
1 check passed
@GentBajko
GentBajko deleted the fix/skill-frontmatter-yaml branch August 30, 2026 02:39

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="skills/core/scripts/lint-sync.sh" line_range="512" />
<code_context>
+#      check here still passes. Prefer ruby (macOS ships psych); fall
+#      back to python's yaml, then to a targeted scan for the traps.
+for f in skills/*/SKILL.md; do
+  fm=$(sed -n '/^---$/,/^---$/p' "$f" | sed '1d;$d')
+  if command -v ruby >/dev/null 2>&1; then
+    printf '%s\n' "$fm" | ruby -ryaml -e 'YAML.safe_load(STDIN.read)' >/dev/null 2>&1 \
</code_context>
<issue_to_address>
**issue (bug_risk):** The extraction does not verify that each SKILL.md has both opening and closing `---` delimiters. When the closing delimiter is missing, `sed` consumes the rest of the file and removes only the first and last lines, so a file whose remaining content is blank or comments can pass YAML validation despite having malformed frontmatter.

**Triggers:** When a SKILL.md is missing its closing frontmatter delimiter and the remaining body is YAML-compatible.

**Suggested fix:** Check that exactly two delimiter lines are present and fail before parsing when either delimiter is missing or the delimiters are not in the expected positions.
</issue_to_address>

### Comment 2
<location path="skills/core/scripts/lint-sync.sh" line_range="514-519" />
<code_context>
+for f in skills/*/SKILL.md; do
+  fm=$(sed -n '/^---$/,/^---$/p' "$f" | sed '1d;$d')
+  if command -v ruby >/dev/null 2>&1; then
+    printf '%s\n' "$fm" | ruby -ryaml -e 'YAML.safe_load(STDIN.read)' >/dev/null 2>&1 \
+      || err "$f frontmatter is not valid YAML"
+  elif command -v python3 >/dev/null 2>&1 \
+       && python3 -c 'import yaml' >/dev/null 2>&1; then
+    printf '%s\n' "$fm" | python3 -c 'import yaml,sys; yaml.safe_load(sys.stdin)' >/dev/null 2>&1 \
+      || err "$f frontmatter is not valid YAML"
+  else
</code_context>
<issue_to_address>
**issue (bug_risk):** The new check only tests whether the extracted block parses as YAML; it never verifies that required `name` and `description` keys survive the parse. A SKILL.md with valid YAML frontmatter containing only unrelated keys therefore passes check 12m, contrary to the stated requirement that name and description be asserted.

**Triggers:** When a skill has syntactically valid frontmatter but omits `description` (or otherwise omits required metadata).

**Suggested fix:** Capture the parsed mapping and explicitly require non-null `name` and `description` values, while preserving the existing directory-name check for `name`.

```suggestion
    printf '%s\n' "$fm" | ruby -ryaml -e 'm = YAML.safe_load(STDIN.read); abort unless m.is_a?(Hash) && !m["name"].nil? && !m["description"].nil?' >/dev/null 2>&1 \
      || err "$f frontmatter is not valid YAML"
  elif command -v python3 >/dev/null 2>&1 \
       && python3 -c 'import yaml' >/dev/null 2>&1; then
    printf '%s\n' "$fm" | python3 -c 'import yaml,sys; m=yaml.safe_load(sys.stdin); sys.exit(0 if isinstance(m,dict) and m.get("name") is not None and m.get("description") is not None else 1)' >/dev/null 2>&1 \
      || err "$f frontmatter is not valid YAML"
```
</issue_to_address>

### Comment 3
<location path="skills/core/scripts/lint-sync.sh" line_range="513" />
<code_context>
+#      back to python's yaml, then to a targeted scan for the traps.
+for f in skills/*/SKILL.md; do
+  fm=$(sed -n '/^---$/,/^---$/p' "$f" | sed '1d;$d')
+  if command -v ruby >/dev/null 2>&1; then
+    printf '%s\n' "$fm" | ruby -ryaml -e 'YAML.safe_load(STDIN.read)' >/dev/null 2>&1 \
+      || err "$f frontmatter is not valid YAML"
+  elif command -v python3 >/dev/null 2>&1 \
</code_context>
<issue_to_address>
**issue (bug_risk):** The availability test selects Ruby solely from `command -v ruby`; if Ruby is installed without its YAML/Psych library, `ruby -ryaml` fails for every file and the script reports invalid YAML instead of trying the available Python YAML fallback.

**Triggers:** When `ruby` is installed but the `yaml`/Psych library is unavailable and Python with PyYAML is installed.

**Suggested fix:** Probe `ruby -ryaml -e ...` before selecting the Ruby branch, or fall through to the Python branch when the Ruby parser invocation cannot load.

```suggestion
  if command -v ruby >/dev/null 2>&1 \
     && ruby -ryaml -e 'exit' >/dev/null 2>&1; then
```
</issue_to_address>

### Comment 4
<location path="skills/core/scripts/lint-sync.sh" line_range="521-522" />
<code_context>
+    printf '%s\n' "$fm" | python3 -c 'import yaml,sys; yaml.safe_load(sys.stdin)' >/dev/null 2>&1 \
+      || err "$f frontmatter is not valid YAML"
+  else
+    printf '%s\n' "$fm" | grep -q ': .*: ' \
+      && err "$f frontmatter has a plain scalar containing \": \" (breaks YAML)"
+  fi
+done
</code_context>
<issue_to_address>
**issue (bug_risk):** The parser-free fallback flags any line matching `: .*: `, including valid quoted YAML such as `description: "Use foo: bar"`; with neither Ruby nor PyYAML available, valid frontmatter is reported as invalid solely because the quoted scalar contains colon-space.

**Triggers:** When the host has neither a usable Ruby YAML parser nor Python's yaml module and a description contains `: ` inside quotes.

**Suggested fix:** Make the fallback distinguish quoted scalars from unquoted plain scalars, or report that YAML validation is unavailable instead of rejecting valid quoted YAML.

```suggestion
    err "$f frontmatter YAML validation unavailable (install ruby or python3 with PyYAML)"
```
</issue_to_address>

Sourcery assessment

Approval pending. 4 findings to address first.

Blocking findings: skills/core/scripts/lint-sync.sh:512, skills/core/scripts/lint-sync.sh:519, skills/core/scripts/lint-sync.sh:513, skills/core/scripts/lint-sync.sh:522


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

# check here still passes. Prefer ruby (macOS ships psych); fall
# back to python's yaml, then to a targeted scan for the traps.
for f in skills/*/SKILL.md; do
fm=$(sed -n '/^---$/,/^---$/p' "$f" | sed '1d;$d')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The extraction does not verify that each SKILL.md has both opening and closing --- delimiters. When the closing delimiter is missing, sed consumes the rest of the file and removes only the first and last lines, so a file whose remaining content is blank or comments can pass YAML validation despite having malformed frontmatter.

Triggers: When a SKILL.md is missing its closing frontmatter delimiter and the remaining body is YAML-compatible.

Suggested fix: Check that exactly two delimiter lines are present and fail before parsing when either delimiter is missing or the delimiters are not in the expected positions.

Comment on lines +514 to +519
printf '%s\n' "$fm" | ruby -ryaml -e 'YAML.safe_load(STDIN.read)' >/dev/null 2>&1 \
|| err "$f frontmatter is not valid YAML"
elif command -v python3 >/dev/null 2>&1 \
&& python3 -c 'import yaml' >/dev/null 2>&1; then
printf '%s\n' "$fm" | python3 -c 'import yaml,sys; yaml.safe_load(sys.stdin)' >/dev/null 2>&1 \
|| err "$f frontmatter is not valid YAML"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The new check only tests whether the extracted block parses as YAML; it never verifies that required name and description keys survive the parse. A SKILL.md with valid YAML frontmatter containing only unrelated keys therefore passes check 12m, contrary to the stated requirement that name and description be asserted.

Triggers: When a skill has syntactically valid frontmatter but omits description (or otherwise omits required metadata).

Suggested fix: Capture the parsed mapping and explicitly require non-null name and description values, while preserving the existing directory-name check for name.

Suggested change
printf '%s\n' "$fm" | ruby -ryaml -e 'YAML.safe_load(STDIN.read)' >/dev/null 2>&1 \
|| err "$f frontmatter is not valid YAML"
elif command -v python3 >/dev/null 2>&1 \
&& python3 -c 'import yaml' >/dev/null 2>&1; then
printf '%s\n' "$fm" | python3 -c 'import yaml,sys; yaml.safe_load(sys.stdin)' >/dev/null 2>&1 \
|| err "$f frontmatter is not valid YAML"
printf '%s\n' "$fm" | ruby -ryaml -e 'm = YAML.safe_load(STDIN.read); abort unless m.is_a?(Hash) && !m["name"].nil? && !m["description"].nil?' >/dev/null 2>&1 \
|| err "$f frontmatter is not valid YAML"
elif command -v python3 >/dev/null 2>&1 \
&& python3 -c 'import yaml' >/dev/null 2>&1; then
printf '%s\n' "$fm" | python3 -c 'import yaml,sys; m=yaml.safe_load(sys.stdin); sys.exit(0 if isinstance(m,dict) and m.get("name") is not None and m.get("description") is not None else 1)' >/dev/null 2>&1 \
|| err "$f frontmatter is not valid YAML"

# back to python's yaml, then to a targeted scan for the traps.
for f in skills/*/SKILL.md; do
fm=$(sed -n '/^---$/,/^---$/p' "$f" | sed '1d;$d')
if command -v ruby >/dev/null 2>&1; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The availability test selects Ruby solely from command -v ruby; if Ruby is installed without its YAML/Psych library, ruby -ryaml fails for every file and the script reports invalid YAML instead of trying the available Python YAML fallback.

Triggers: When ruby is installed but the yaml/Psych library is unavailable and Python with PyYAML is installed.

Suggested fix: Probe ruby -ryaml -e ... before selecting the Ruby branch, or fall through to the Python branch when the Ruby parser invocation cannot load.

Suggested change
if command -v ruby >/dev/null 2>&1; then
if command -v ruby >/dev/null 2>&1 \
&& ruby -ryaml -e 'exit' >/dev/null 2>&1; then

Comment on lines +521 to +522
printf '%s\n' "$fm" | grep -q ': .*: ' \
&& err "$f frontmatter has a plain scalar containing \": \" (breaks YAML)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The parser-free fallback flags any line matching : .*: , including valid quoted YAML such as description: "Use foo: bar"; with neither Ruby nor PyYAML available, valid frontmatter is reported as invalid solely because the quoted scalar contains colon-space.

Triggers: When the host has neither a usable Ruby YAML parser nor Python's yaml module and a description contains : inside quotes.

Suggested fix: Make the fallback distinguish quoted scalars from unquoted plain scalars, or report that YAML validation is unavailable instead of rejecting valid quoted YAML.

Suggested change
printf '%s\n' "$fm" | grep -q ': .*: ' \
&& err "$f frontmatter has a plain scalar containing \": \" (breaks YAML)"
err "$f frontmatter YAML validation unavailable (install ruby or python3 with PyYAML)"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant