AgentsOpen SourceFreeActiveMachine-verified· beginner · ~10 min setup

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.

by Shilpa Mitra· verified today· v1.0.0

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. 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. 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 today
  • package-okcontainstimeout 30s · max $0

    Expected: skills package OK: 3 skill(s) valid (frontmatter + dir match); plugin finding-unknowns in marketplace manifest

  • clean-exitexit_codetimeout 30s · max $0

    Expected: 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

Liked this workflow?

Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).