Appearance
The validate-skills action
.github/actions/validate-skills is this repository's validator for repositories that host agent skills. Vivswan/repo-platform's own ci.yml runs it on repo-platform's skills directory; this repository runs it on itself in checks.yml.
The layout it validates:
text
skills/ # the skills directory (input skills-dir, default `skills`)
README.md # index of the hosted skills; required as soon as the directory exists
my-skill/
SKILL.md # YAML frontmatter: name (= folder, kebab-case), description
... # whatever else the skill carries
.claude-plugin/
plugin.json # the published catalog (input plugin-manifest); its `skills` array lists `./skills/my-skill`
marketplace.json # optional; checked for consistency with plugin.json when presentA skill folder is unpublished until plugin.json lists it. Installers and discovery mode read the manifest, not the disk, so an unlisted folder validates green and never ships.
Inputs
| Input | Meaning | Default |
|---|---|---|
skills-dir | Directory holding the skills, relative to the repo root | skills |
plugin-manifest | Path of the plugin manifest listing the published skills | .claude-plugin/plugin.json |
mode | structure or discovery | structure |
Both paths must be plain relative paths inside the checkout: no absolute paths, no . or .. segments.
What each mode checks
An empty catalog ("skills": []) passes both modes: a freshly adopted repository publishes nothing yet.
| Mode | Network | What a red means |
|---|---|---|
structure | offline | the catalog structure is broken; cheap, so it belongs in the merge gate |
discovery | needs npm | the real npx -y skills@<pinned> add . --list does not list every skill in plugin.json |
Structure mode, in the order it reports:
plugin.jsonparses and has a kebab-casename.- Each path in its
skillsarray is a real direct child of the skills directory with aSKILL.md. - The skills directory is a real directory or absent. Anything else at the path is an error.
- The skills directory carries an index
README.mdat its root. - Every folder under the skills directory, listed or not, has a
SKILL.mdwith frontmatter. - Its
nameequals the folder and is kebab-case, 64 characters max. - Its
descriptionis nonempty, 1024 characters max. - Its frontmatter does not set
metadata.internaltotrue: the skills CLI silently drops such a skill at install time, so the value on a published skill hides it from consumers while every gate stays green. Any other value publishes. - A
.mcp.jsonin a skill folder, when present, parses as JSON. marketplace.json, when present next to the manifest, has a kebab-casenameand a non-emptypluginslist.- Each marketplace plugin's
sourcepath stays inside the repository; a source object naming another repository (git-subdir,github,url) is left toclaude plugin validate, since nothing of it is in the checkout. - A marketplace plugin's
skills(one path or a list) resolve under itssourceand must stay inside it, each a kebab-case folder with aSKILL.md; the root plugin (source: "./") keeps the repository's skills directory rule. - A plugin publishing the repository root carries the same
nameasplugin.json. - Symlinks are rejected anywhere on a validated path, ancestors included. A link can point outside the checkout, so what ships would not be what was validated.
- The one exception: a marketplace plugin's
sourcemay pass through in-repo links while its physical path stays inside the repository.
Discovery mode downloads the CLI from the npm registry at the exact version validate-skills.ts records in SKILLS_CLI_VERSION (a CLI release cannot red a green catalog; the pin moves by hand), makes up to three attempts, and matches each published skill name on word boundaries in the listing. Give it its own job; a caller decides whether discovery gates its merges (repo-platform gates it).
Exit codes:
| Exit | Meaning |
|---|---|
0 | clean |
1 | one error: line per finding, then a count |
2 | unknown mode |
The full rule set is the source, validate-skills.ts. Its tests are tests/repo/validate-skills.test.ts.
How repo-platform calls it
repo-platform's ci.yml pins the action to a commit on main (the comment records which main it took) and passes its skills directory:
yaml
- uses: Vivswan/skills/.github/actions/validate-skills@<sha> # main, <date>
with:
skills-dir: skills
mode: structureThe action sets up bun through the fleet's shared bun-setup action, which walks up from the action's folder to this repository's own root .bun-version, the pin the fleet sync keeps current. The bun pin never comes from the caller's checkout. The caller needs no bun step and no install step.
How this repository runs it on itself
- CI: the
validate-skills-actionjob in.github/workflows/checks.ymlruns the composite action on this checkout instructuremode, inside the all-green gate. That job exercises the action as repo-platform consumes it,action.ymland the root bun pin included. - Locally and in the pre-commit hook,
bun run validateruns the same script with the defaults (bun run checkincludes it):
sh
SKILLS_DIR=skills PLUGIN_MANIFEST=.claude-plugin/plugin.json MODE=structure bun .github/actions/validate-skills/validate-skills.ts- Discovery, the same command with
MODE=discovery(needs network). bun run testcovers the action's unit tests along with the rest of the suite.
The action is the baseline any repository hosting skills can call. Everything specific to this catalog (per-skill README and codex manifest, template/, the marketplace shape with its upstream entries, cross-file drift) is bun run smoke, scripts/smoke.mts.