Verify an agent-skills plugin before you ship or install it
Check that a Claude Code / agentskills.io skills package is structurally valid, every SKILL.md has proper frontmatter and a name matching its folder, and the plugin marketplace manifest parses, so a broken skill never fails to load after you publish it.
Run this workflow
CI-verified, 2/2 fixtures passing.
Build this with your agent
One copy-paste hands Claude Code, Codex, or Cursor the full recipe, steps included, nothing to fetch.
Intended Use
Anyone authoring or vetting an agent-skills plugin. CI checks each skills/<name>/SKILL.md starts with frontmatter, carries a name and a real description, and the name matches its directory, and that the .claude-plugin marketplace manifest parses and names at least one plugin. No install, no agent call. Whether a skill triggers correctly is fenced (that is description tuning).
Not for
- Confirming a skill fires at the right time, that depends on the description field and the model, which CI cannot grade; test triggering by hand
- Judging skill quality, this checks the package loads, not that the instructions are good
- Permission or dual-use safety, for that see vet-agent-skill-before-install; this is a packaging check
The Stack
Tested Against
agentskills.io SKILL.md formatClaude Code plugin manifestruby@3.x (YAML + JSON stdlib)Side effects & data flow
- Network
- none, local only
- Writes
- ./skills/, ./.claude-plugin/
- Credentials
- none required
Prerequisites
- A skills repo laid out as skills/<name>/SKILL.md with a .claude-plugin manifest
Steps
- 1
Lay out the package and validate frontmatter + manifest
Each skill is skills/<name>/SKILL.md with name + description frontmatter, and the plugin is declared in .claude-plugin/marketplace.json. CI checks every SKILL.md parses, its name matches its folder, and the manifest names a plugin. Actually installing it with /plugin install and confirming triggering are fenced.
mkdir -p skills/blindspot-pass skills/interview-me skills/change-quiz .claude-plugin cat > skills/blindspot-pass/SKILL.md <<'EOF' --- name: blindspot-pass description: Surface the user's unknown unknowns before work starts, in an unfamiliar codebase area or domain. --- # Blindspot pass Find what the user does not know they do not know, then help them prompt better. EOF cat > skills/interview-me/SKILL.md <<'EOF' --- name: interview-me description: Interview the user one question at a time, architecture-changing questions first, to resolve ambiguity before implementation. --- # Interview me Ask one question per turn, biggest blast radius first. EOF cat > skills/change-quiz/SKILL.md <<'EOF' --- name: change-quiz description: After a working session, report what changed and quiz the user before they merge, so unread changes do not ship. --- # Change quiz Report the change, then a quiz the user must pass to merge. EOF cat > .claude-plugin/marketplace.json <<'EOF' { "name": "finding-unknowns-skills", "owner": { "name": "Neeeophytee" }, "plugins": [ { "name": "finding-unknowns", "source": "./", "description": "Skills for finding your unknowns before they get expensive." } ] } EOF ruby -rjson -ryaml -e ' skills = Dir.glob("skills/*/SKILL.md").sort abort "BAD: no SKILL.md files found" if skills.empty? ok = 0 skills.each do |f| raw = File.read(f, encoding: "UTF-8") abort "BAD: " + f + " does not start with frontmatter" unless raw.start_with?("---") fm = YAML.safe_load(raw.split("---")[1]) abort "BAD: " + f + " missing name" if fm["name"].to_s.empty? abort "BAD: " + f + " description too short" if fm["description"].to_s.length < 20 dir = File.basename(File.dirname(f)) abort "BAD: " + f + " name does not match its directory " + dir unless fm["name"] == dir ok += 1 end mk = JSON.parse(File.read(".claude-plugin/marketplace.json")) abort "BAD: marketplace manifest missing name" if mk["name"].to_s.empty? plugins = mk["plugins"] || [] abort "BAD: no named plugin in the marketplace manifest" if plugins.empty? || plugins[0]["name"].to_s.empty? puts "skills package OK: " + ok.to_s + " skill(s) valid (frontmatter + dir match); plugin " + plugins[0]["name"] + " in marketplace manifest" ' - 2
Install and test triggering (the agent step, not checked by CI)
Publish the repo, then /plugin marketplace add <owner>/<repo> and /plugin install <plugin>@<repo>. Confirm each skill fires when you expect and stays quiet otherwise, that is description tuning, which is fenced. If a skill over-fires, tighten its description and re-validate.
Eval, 2 fixtures
Last passed: verified todaypackage-okcontainstimeout 30s · max $0Expected:
skills package OK: 3 skill(s) valid (frontmatter + dir match); plugin finding-unknowns in marketplace manifestclean-exitexit_codetimeout 30s · max $0Expected:
0
Results
Skills only trigger if their frontmatter is right: the description is what fires them, and a name that does not match its folder will not load. This is the check we run on our own finding-unknowns-skills repo (8 skills distilled from Thariq Shihipar's unknowns essay) before publishing. It validates the packaging spine; whether a skill fires at the right moment is a description-tuning judgement, not something CI can grade.
Did this work for you?
Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.
Related workflows
- WebMCP: declare a site's agent tools, and gate the ones that spend money
- Wire GLM-5.2 into Hermes: valid route, 64k-context check, no key in config
- Voyager pattern: validate a procedural skill store before you trust a saved skill
- Hermes MoA: stack frontier models into one virtual model for hard turns
- promptfoo: make agent evals fail the build, not the user
- E2B: run model-written code in a sandbox, not on your box
Liked this workflow?
Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).