Skip to content

Plugins

Kelly Ferrone edited this page Sep 21, 2026 · 4 revisions

Plugins

A plugin is a folder of skills and prompts: a path under a source, plus a description, a category and tags. Plugins are what libraries pick from, and every field is in Configuration.

plugins:
- name: house-runbooks
  description: How we run things here.
  category: operations
  tags: [homelab, runbooks]
  source: github://acme/handbook//runbooks?ref=main
  skills: ["*"]
  prompts: []

The metadata fields — description, category, tags, keywords, version — are Claude's marketplace-entry fields, deliberately. A plugin you declare and a plugin read out of somebody's marketplace.json are the same kind of thing, and a query cannot tell them apart.

The address

A plugin's source is an address, not a URL:

<source>://<path>[//<subdir>][?ref=<ref>]

The scheme is the name of a declared source — so github://grafana/skills means "the path grafana/skills under the source called github". The one built-in is file://, which needs no source entry and takes an absolute path: file:///srv/prompts/grafana.

// opens a subdirectory and ?ref= pins a revision, which is hashicorp go-getter's grammar — the one kustomize and Terraform read — so github://acme/handbook//runbooks?ref=main means there what it means here. ?ref= needs a git source; anything else in the query is refused rather than ignored, because a typo'd ?rev= that quietly served the default branch is how nobody finds out.

The subdirectory is not part of the fetch. What is materialised is the path and the ref; the subdirectory only says where to look inside it. Two plugins on one repository at one ref are one clone and one export, however many libraries they end up in — which is what makes a seven-entry catalogue cheap.

What a plugin serves

skills, prompts and files are glob lists relative to the plugin root, and each has three states:

The key Means
absent ask the plugin's plugin.json, then fall back to the conventions
a list exactly this, instead of the conventions
[] serve none of this kind

The conventions, when nothing says otherwise:

Kind Conventions Served as
skills skills/**/SKILL.md, .github/skills/*/SKILL.md, .claude/skills/*/SKILL.md, .agents/skills/*/SKILL.md skills
prompts prompts/**/*.md, .github/prompts/*.prompt.md, commands/*.md prompts
files none — library files are opt-in skill://<library>/<path>

A skills glob may name the SKILL.md, the skill's directory, or a directory of directories; a matched directory contributes every skill beneath it. A pattern whose last component is ** is normalised to **/*, because before Python 3.13 a trailing ** matches directories only and would silently serve nothing.

prompts: [] is the one to remember. A repository of skills usually also keeps prompts/ for its own development, and those are not yours to serve.

The manifest

A plugin that ships a plugin.json is telling you what it publishes, so it is read before the conventions are:

It is looked for at .claude-plugin/plugin.json and then plugin.json, first found, and four of its fields are read. skills and commands are the components. keywords join the plugin's labels — the things a pluginSelector and a request's ?tags= match — which is how a kit that is tagged nowhere else becomes selectable by what its author called it. description and version fill in only what the marketplace entry or your config left empty: a declared value always wins, and a marketplace entry is completed, never overridden. Everything else it carries is ignored here.

A plugin you declare against the same repository reads the same manifest, so it inherits those labels and that version too — which is worth knowing before you scope by tag, since the helper plugin will answer to the kit's keywords.

commands becomes this server's prompts: a file is itself, a directory is every markdown file in it, which is what a command directory means to the client that publishes it. A path that escapes the plugin root is dropped — a published manifest is fetched content, and one bad entry must not cost the whole plugin — while a manifest that is not readable JSON fails the plugin outright and says so in /health.

Your own skills: or prompts: wins over the manifest, per kind. Declaring skills: and leaving prompts out is a plugin whose skills you chose and whose commands its author did.

The plugin root

The plugin root is the tree the fetch materialised, joined with the address's subdirectory. It is the ceiling for everything the plugin serves, and it is also what ${CLAUDE_PLUGIN_ROOT} means in Claude Code — "resources shared between the plugin's skills".

So it is what that placeholder becomes here, on every render:

Placeholder Becomes Where
${CLAUDE_PLUGIN_ROOT} skill://<library> a SKILL.md body, a prompt body
${CLAUDE_SKILL_DIR} skill://<library>/<folder>/<name> a SKILL.md body — a command is not a skill

A body that writes ${CLAUDE_PLUGIN_ROOT}/shared/tokens.md therefore renders as an address a reader can actually read. Every other ${…} belongs to the client and is left exactly as written — see Prompts.

A file under the plugin root is readable even when nothing listed it. Any non-hidden file under the root can be read at skill://<library>/<path>, and at skill://<library>/<folder>/<skill>/<path> when the skill itself has no file at that path. That is what makes a skill citing shared/api.md from its repository's root resolve rather than dangle. The files: globs are still what _files.md lists: the fallback is for a citation that would otherwise miss, not a way to publish a tree by accident.


Home · Config file · Sources · Libraries · Skills · Configuration

Clone this wiki locally