chore(ci): block "./skills" regression — validator + CI gate (#686 follow-up) (#690)

Issue #686 was the second round of the same Claude Code path-validator
tightening: v2.1.107 rejected bare "./" (fixed in #539 by moving to
"./skills"), then v2.1.133 also rejected "./skills". The validator that
codified the #539 fix was still recommending "./skills" verbatim — so a
future round 3 would have hit the same trap.

This commit makes the validator catch the regression and runs it in CI:

- scripts/check_plugin_json.py
  - Reject any "skills" string starting with "./" (catches both
    "./skills" and "./skills/sub" patterns)
  - Update docstring + error message to point at the layout-correct
    forms instead of the now-broken "./skills"
  - Recognize "source" and "attribution" as approved extension fields
    (already documented in CLAUDE.md but not in the validator), so the
    21 pre-existing false-positives go away and CI can run blocking
  - Drop the "./" rejection inside arrays — CLAUDE.md says ["./"] is
    the correct single-skill-at-root form

- .github/workflows/ci-quality-gate.yml
  - Add blocking "Validate plugin.json manifests" step that runs the
    validator on every PR

- CLAUDE.md
  - Add an Enforcement note pointing at the validator and the lockstep
    rule: when CC tightens its path validator again, update validator
    rules and CLAUDE.md together

Verified: 69/69 manifests pass; 6-case smoke test confirms validator
rejects all three known-broken forms ("./skills", "./", "./skills/sub")
and accepts all three documented-valid forms ("skills", ["./"],
explicit array).

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Alireza Rezvani 2026-05-19 05:54:56 +02:00 committed by GitHub
parent daa88bb299
commit f7bb1f86bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 28 additions and 7 deletions

View file

@ -82,6 +82,10 @@ jobs:
engineering-team ra-qm-team engineering \
business-growth finance project-management scripts
- name: Validate plugin.json manifests (blocking — guards #539 + #686)
run: |
python scripts/check_plugin_json.py --all
- name: Run test suite
run: |
python -m pytest tests/ --tb=short -q

View file

@ -337,6 +337,8 @@ This repository publishes skills to **ClawHub** (clawhub.com) as the distributio
- Single-skill plugin (SKILL.md at root): `"skills": ["./"]` (array form required).
- Plugin with `skills/` subdir: `"skills": "skills"` (no `./` prefix — see issue #686).
- Multi-skill domain plugin (skills are subfolders at root): `"skills": ["./sub1", "./sub2", ...]` (explicit list, omit `"./"` to avoid namespace collision with the index SKILL.md).
**Enforcement:** `scripts/check_plugin_json.py --all` runs in `ci-quality-gate.yml` on every PR and blocks merge on any violation. It actively rejects the `"./"` (issue #539) and `"./skills"` (issue #686) regressions. When CC tightens its path validator again in the future, update both the validator's `_check_skills_string` rules and this section together — they must move in lockstep.
6. **Version follows repo versioning.** ClawHub package versions must match the repo release version (currently v2.7.0+).
## Anti-Patterns to Avoid

View file

@ -1,11 +1,22 @@
#!/usr/bin/env python3
"""Validate plugin.json files against the strict ClawHub schema.
Required fields (exactly these 8, no others):
Required fields (exactly these 8):
name, description, version, author{name,url}, homepage, repository, license, skills
skills: must be either a string ("./skills") or an array of relative paths.
The bare "./" form is REJECTED (Claude Code v2.1.107+ rejects it).
Two approved extension fields (documented in CLAUDE.md, stripped at ClawHub-publish):
source, attribution
skills layouts (Claude Code tightens its path validator regularly be explicit):
- Single-skill plugin (SKILL.md at root): "skills": ["./"] (array form)
- Plugin with skills/ subdir: "skills": "skills" (NO "./" prefix #686)
- Multi-skill domain plugin (subfolders at root):
"skills": ["./sub1", "./sub2", ...]
REJECTED forms and why:
- "skills": "./" Claude Code v2.1.107+ rejects ("Path escapes plugin directory")
- "skills": "./skills" Claude Code v2.1.133+ rejects (issue #686)
- Any string starting with "./" (lifted out of array context)
"""
import argparse
import json
@ -15,6 +26,7 @@ import sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ALLOWED = {"name", "description", "version", "author", "homepage", "repository", "license", "skills"}
APPROVED_EXTENSIONS = {"source", "attribution"}
STRING_FIELDS = ("name", "description", "homepage", "repository", "license")
SEMVER = re.compile(r"^\d+\.\d+\.\d+(?:-[\w.]+)?$")
@ -22,7 +34,7 @@ SEMVER = re.compile(r"^\d+\.\d+\.\d+(?:-[\w.]+)?$")
def _check_keys(data):
keys = set(data.keys())
errors = []
extra = keys - ALLOWED
extra = keys - ALLOWED - APPROVED_EXTENSIONS
missing = ALLOWED - keys
if extra:
errors.append(f"extra fields: {sorted(extra)}")
@ -63,7 +75,12 @@ def _check_author(data):
def _check_skills_string(s):
if s in ("./", ""):
return ['skills: "./" is rejected by Claude Code v2.1.107+; use "./skills" or an array']
return ['skills: bare "./" is rejected by Claude Code v2.1.107+; '
'use ["./"] (array) for single-skill at root, or "skills" for subdir layout']
if s.startswith("./"):
return [f'skills: {s!r} starts with "./" — Claude Code v2.1.133+ rejects this as '
f'"Path escapes plugin directory" (issue #686). Drop the "./" prefix: '
f'"{s[2:]}". (For single-skill plugins, use ["./"] in an array instead.)']
return []
@ -74,8 +91,6 @@ def _check_skills_array(s):
for entry in s:
if not isinstance(entry, str):
errors.append(f"skills: entries must be strings, got {entry!r}")
elif entry == "./":
errors.append('skills: "./" is rejected by Claude Code v2.1.107+; list explicit subfolders')
return errors