Lesson 24 of 32
Plugins
Bundling skills, hooks, subagents and MCP servers into one installable unit, so a second repository or a new teammate gets your whole setup at once.
By now you have accumulated things. A CLAUDE.md, two or three skills, a hook
that blocks edits to generated files, a subagent that triages test failures, an
MCP server for your issue tracker. All of it lives in one repository's
.claude/ directory and one settings file, and all of it works.
Then you start a second repository that needs the same setup. Or a colleague asks how you got yours, and the honest answer is a list of six files to copy.
That is the problem plugins solve, and it is worth being clear that it is the only problem they solve. A plugin adds no capability. It is a packaging format.
What a plugin is
A plugin is a directory. Inside it, a manifest gives it a name, and the components sit beside the manifest in directories named for what they hold.
release-tools/
├── .claude-plugin/
│ └── plugin.json
├── skills/
│ └── changelog/
│ └── SKILL.md
├── agents/
│ └── release-checker.md
├── hooks/
│ └── hooks.json
└── .mcp.jsonThe manifest is small:
{
"name": "release-tools",
"description": "Changelog, release checks and the tag workflow we use",
"version": "1.0.0"
}Each component is the same file you would have written anyway. skills/ holds
skill directories with a SKILL.md in each. agents/ holds subagent definitions
in the markdown-with-frontmatter shape from the last lesson. .mcp.json declares
MCP servers. The one that changes is hooks: instead of living under a hooks
key in a settings file, they move into hooks/hooks.json — the same JSON, in a
file of its own.
Two details save an afternoon each. Only plugin.json goes inside
.claude-plugin/; every other directory belongs at the plugin root, and getting
this wrong produces a plugin that installs cleanly and does nothing. And plugin
skills are namespaced by the plugin's name, so the skill above is invoked as
/release-tools:changelog. That prefix is why two plugins can both ship a
/review without a fight, and why the manifest's name field is public
identity rather than decoration.
Turning what you have into one
You do not need a marketplace to test any of this. The --plugin-dir flag loads
a directory as a plugin for one session, which is the whole development loop.
WhyThe setup: this repository has three skills, a protect-generated hook and a test-triage subagent, built up over a month. A second service now needs the same treatment.
You typed: Package the release skills, the protect-generated hook and the test-triage agent as a plugin called release-tools. Leave the originals in place for now.
Claude ran: Write release-tools/.claude-plugin/plugin.json
Claude ran: Run: cp -r .claude/skills/changelog release-tools/skills/
Claude ran: Run: cp .claude/agents/test-triage.md release-tools/agents/
Claude ran: Write release-tools/hooks/hooks.json
WhyThe skills and the agent were copied unchanged. The hook was rewritten, because a hook in a settings file moves into hooks/hooks.json when it becomes part of a plugin. That is the only format change in the whole exercise.
You typed: claude --plugin-dir ./release-tools
You typed: /release-tools:changelog
Output: Reading commits since v2.4.0 ... Wrote CHANGELOG.md (11 entries)WhyThe command is namespaced now. Notice that the un-namespaced /changelog also still works, because the original is still sitting in .claude/skills/. Two copies of the same skill is exactly the confusion you would expect, which is why you delete the originals once the plugin is proven.
You typed: Now make me a marketplace in this repo that lists this plugin, and validate it.
Claude ran: Write .claude-plugin/marketplace.json
Claude ran: Run: claude plugin validate ./release-tools
Output: Validation passedWhyValidation before distribution, for the same reason you run tests before pushing. It catches the misplaced directory described above, which is otherwise diagnosed by watching nothing happen.
While you are iterating, /reload-plugins picks up your changes without a
restart.
Marketplaces
A marketplace is a catalogue: one JSON file listing plugins and where to fetch
each one. It lives at .claude-plugin/marketplace.json in a repository, and it
is genuinely just this shape.
{
"name": "acme-tools",
"owner": { "name": "Platform team" },
"plugins": [
{
"name": "release-tools",
"source": "./release-tools",
"description": "Changelog, release checks and the tag workflow we use"
}
]
}A plugin's source can be a path in the same repository, another Git
repository, an npm package or a zip archive, which means the catalogue and the
plugins do not have to live together.
Using one is two steps, because registering a catalogue and installing from it are separate decisions:
/plugin marketplace add acme-corp/claude-plugins
/plugin install release-tools@acme-toolsThe install asks you for a scope, and it is the same choice you have made everywhere: yourself across all projects, this repository for everyone who works on it, or this repository for you alone. Anthropic maintains public marketplaces too — an official one that Claude Code registers for you, holding things like the language-server plugins that give Claude real type errors, and integrations for GitHub, Sentry and Linear.
For a team, a private repository works exactly the same way; Claude Code uses
the Git credentials you already have. The stronger move is to name the plugin
under enabledPlugins in the project's committed settings, so cloning the
repository is the whole setup.
The version field in the manifest is how you control updates. People receive a
new copy when you change that field, so bumping it is the act of publishing, and
a plugin whose version never moves never updates for anybody. That is a feature
rather than an oversight: the people using your plugin are not surprised by your
work in progress.
What else can go in one
Four component directories cover most of what you will write, but a plugin is also the distribution format for two things you have not met as loose files.
An .lsp.json file connects Claude to a language server, which is where the
official marketplace's per-language plugins come from. That is the difference
between Claude grepping for where a symbol is defined and Claude asking the tool
that already knows, and it means type errors come back after every edit rather
than when someone runs a build. A bin/ directory puts executables on the path
Claude's shell tool sees, so a plugin can ship the tool its skills depend on
instead of documenting the install step in a README.
A plugin can also carry a small settings.json of defaults applied when it is
enabled, which is how a plugin can make one of its own subagents the session's
main agent. That is a large amount of behaviour to hand over in a single install,
and worth remembering when the plugin is someone else's.
When to package, and when not to
The trigger is specific: a second repository needs the same setup, or another person does. Before that point, packaging costs you a namespace prefix, a version to remember to bump, an install step and a layer of indirection when something misbehaves, in exchange for nothing you can use yet.
So build in .claude/ while the thing is still finding its shape. Convert when
it has stopped changing weekly and someone else wants it. The exception worth
making is the one where the package is the point: a set of skills and agents
that only makes sense together, like a review toolkit or a commit workflow, is
easier to reason about as one named thing from the beginning.
Two honest limitations for the day you do convert. A subagent shipped in a
plugin cannot use the frontmatter fields for hooks, MCP servers or permission
mode — those are ignored for security reasons, and an agent that needs them has
to be copied into .claude/agents/ instead. And a project or personal subagent
with the same name overrides the plugin's, which is usually what you want and
occasionally the reason your plugin appears to be ignored.
The other direction deserves the same honesty. Installing someone else's plugin is not like installing a document. It can carry hooks, which are shell commands that fire automatically on your machine, and MCP servers, which are processes or external endpoints. The install view lists everything a plugin will add and estimates what it will cost you in context every turn. Read it.
What to take away
A plugin is a directory with a manifest that names it and component directories
beside that manifest — skills/, agents/, hooks/hooks.json, .mcp.json —
holding the same files you would otherwise scatter through .claude/. Only the
manifest goes inside .claude-plugin/, and plugin skills are namespaced under
the plugin's name, which is what lets several plugins coexist. Develop against a
directory with --plugin-dir and /reload-plugins, then distribute through a
marketplace, which is a JSON catalogue of plugins and their sources that people
add once and install from separately. Package something when a second repository
or another person needs it, not before, because everything a plugin contains
already works as loose configuration. And treat installing one as a trust
decision, since hooks and MCP servers arrive with it.
Next: the lesson that ties the whole of this part together — how to tell, for any given problem, which of these six mechanisms is the right one.
Check yourself
5 questions · pass 4/5 to unlock Choosing the Right Extension
1.What problem does packaging your configuration as a plugin actually solve?
2.A plugin named
release-toolsships a skill inskills/changelog/. How do you invoke it?3.You put
skills/andhooks/inside the plugin's.claude-plugin/directory. What happens?4.You have a workflow you are still changing every few days, and nobody else needs it. Where should it live?
5.What is the honest risk of installing someone else's plugin from a marketplace?
5 left to answer