-
Notifications
You must be signed in to change notification settings - Fork 1
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.
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.
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.
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 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
Configuration, Tools and Endpoints are generated by scripts/generate_wiki.py from the config models, a live server's tool list and the OpenAPI document — so they describe the server that shipped, not the one someone remembered. Prose for a generated page goes in wiki/notes/<page>.notes.md.
mcp-kb · MIT
Getting started
The config file
What clients get
Reference