From f7bb1f86bc6d4808a6c6a2ccdfaa002cf5bbf99f Mon Sep 17 00:00:00 2001 From: Alireza Rezvani <5697919+alirezarezvani@users.noreply.github.com> Date: Tue, 19 May 2026 05:54:56 +0200 Subject: [PATCH] =?UTF-8?q?chore(ci):=20block=20"./skills"=20regression=20?= =?UTF-8?q?=E2=80=94=20validator=20+=20CI=20gate=20(#686=20follow-up)=20(#?= =?UTF-8?q?690)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .github/workflows/ci-quality-gate.yml | 4 ++++ CLAUDE.md | 2 ++ scripts/check_plugin_json.py | 29 ++++++++++++++++++++------- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-quality-gate.yml b/.github/workflows/ci-quality-gate.yml index 3c6067aa..adfaee28 100644 --- a/.github/workflows/ci-quality-gate.yml +++ b/.github/workflows/ci-quality-gate.yml @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 42e986bf..e6f6a9d8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/scripts/check_plugin_json.py b/scripts/check_plugin_json.py index c2b5a11a..95a9362f 100755 --- a/scripts/check_plugin_json.py +++ b/scripts/check_plugin_json.py @@ -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