Plugin authoring
Plugins are the next layer above skills/agents/commands — one installable unit that bundles several catalogue entries plus optional hooks and MCP servers. skill_pool exposes them as a per-tenant Claude Code marketplace, served from the same Axum router.
What is a plugin?
A plugin is Claude Code's packaging primitive: it bundles one or more
skills, agents, commands, hooks, MCP servers, LSP servers, and monitors into one installable unit
(official reference).
A marketplace is the catalog Claude Code consumes via /plugin marketplace add <url>.
skill_pool already distributes skills, agents, and commands as atomic units. Plugins sit one layer above: a curator picks N already-registered atomic pieces, packages them into one plugin, and exposes the result through their tenant's marketplace.
| Primitive | What it is | How it ships |
|---|---|---|
| Skill | SKILL.md Claude can invoke by name | ~/.claude/skills/ or inside a plugin |
| Agent | Markdown declaring a subagent | ~/.claude/agents/ or inside a plugin |
| Command | Markdown Claude treats as a slash command | ~/.claude/commands/ or inside a plugin |
| Plugin | Directory with .claude-plugin/plugin.json bundling any of the above | /plugin install <name>@<marketplace> |
End-to-end install flow
Three sourcing modes
| Mode | Curator effort | Bytes stored by skill_pool? | Egress required from dev? |
|---|---|---|---|
| Internal | High (assemble in portal) | Yes | No (skill_pool only) |
| External | Low (paste URL) | No | Yes (upstream URL) |
| Mirror | Medium (paste URL + refresh) | Yes | No (skill_pool only) |
- Internal — authored in skill_pool. Portal composer assembles from registered skills/agents/commands; server materializes the plugin tree and serves it from
/git/plugins/<slug>.git. Versions are git refs. Reuses the most of the existing registry. - External — plugin lives in someone else's repo (public GitHub, internal GitLab). Marketplace entry's
sourceis the upstream URL; Claude clones it directly. Cheapest mode to operate. - Mirror — plugin lives upstream but skill_pool clones it locally, refreshes on a schedule, and serves the mirror. Path for air-gapped tenants, slow upstreams, or curators wanting a snapshot they control.
Author your first plugin
# 1) Lay out the plugin directory
$ mkdir -p acme-platform-toolkit/.claude-plugin
$ cd acme-platform-toolkit
# 2) Write the manifest
$ cat > .claude-plugin/plugin.json << 'JSON'
{
"name": "acme-platform-toolkit",
"version": "1.0.0",
"description": "Acme platform team's review + migration + observability skills",
"author": "platform@acme.com",
"skills": [
{ "slug": "code-reviewer", "version": "1.4.0" },
{ "slug": "sqlx-migrations", "version": "2.0.1" },
{ "slug": "axum-tracing", "version": "0.3.0" }
],
"agents": [{ "slug": "platform-on-call", "version": "0.2.0" }],
"commands": [{ "slug": "/postmortem-stub", "version": "1.0.0" }],
"hooks": {
"SessionStart": [{ "type": "command", "command": "${PLUGIN_DIR}/hooks/load-acme-ctx.sh" }]
}
}
JSON
# 3) Publish
$ skill-pool publish . --kind plugin --version 1.0.0
validating manifest... OK
resolving 3 skills · 1 agent · 1 command... OK (all in tenant)
secret scan (PLUGIN_DIR/hooks/)... OK
packing... OK (12.4 KB)
publishing to marketplace... OK
OK acme-platform-toolkit@1.0.0 published
https://acme.skills.example/.claude-plugin/marketplace.json
https://acme.skills.example/git/plugins/acme-platform-toolkit.git
# 4) Install in Claude Code
User: /plugin marketplace add https://acme.skills.example
User: /plugin install acme-platform-toolkit
OK installed at ~/.claude/plugins/cache/acme/acme-platform-toolkit/1.0.0
Manifest schema (key fields)
| Field | Type | Notes |
|---|---|---|
name | string | Slug; unique per tenant + version |
version | semver | Immutable; republish requires a bump |
skills[] · agents[] · commands[] | array | {slug, version} pairs into the existing catalogue |
hooks | object | SessionStart, Stop, SessionEnd per Claude Code spec |
mcp_servers | array | Embedded MCP server definitions |
monitors · themes · settings | object | Claude Code passthrough fields |
Full schema: docs/plugin-manifest-schema.md.
Mirror & refresh worker
Mirrored plugins carry pull_interval_secs (default 24h). A background worker wakes every 60s,
finds plugins whose last_pulled_at + pull_interval_secs is in the past, pulls up to N in parallel,
and updates the local git endpoint. Pull failures keep the last-good copy serving and surface a warning chip
in the portal.
When SKILL_POOL_REDIS_URL is set, the refresh uses a durable Redis queue with DLQ.
Without it, refreshes spawn an in-process tokio task per call (fail-open, no retry across restarts).
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
/plugin marketplace add returns 404 | Tenant subdomain not resolved | Verify Host header / DNS; curl /.claude-plugin/marketplace.json from the dev machine |
/plugin install hangs | libgit2 HTTP-transport quirk | Issue #73 tracks this; current workaround is to use a system git client |
Republish rejected as 409 conflict | That (slug, version) already exists and is not archived | Bump the version, or archive the prior row first |
| Mirror stays on stale ref | Worker can't reach upstream | Check /v1/plugins/<slug>/health; portal shows last error |