Plugin vs skill
Not competing concepts. A plugin is the box; a skill is one thing in the box.
| Skill | Plugin | |
|---|---|---|
| What it is | One SKILL.md, a unit of knowledge or a workflow |
A directory bundling many components |
| Can contain | Instructions, supporting files, scripts | Skills, subagents, hooks, MCP servers, LSP configs, background monitors, executables, default settings |
| Invocation | /skill-name |
Its components are invoked; the plugin itself is not |
| Name collisions | Possible across scopes, one shadows another | Impossible, namespaced as /plugin:component |
| Distribution | Commit .claude/skills/, or copy by hand |
/plugin install name@marketplace, versioned |
| Updates | Manual | Marketplace |
The namespacing is worth more than it sounds. A team skill called /deploy is
silently shadowed by any
contributor who happens to have a personal /deploy. Packaged as a plugin it
becomes /platform:deploy and the collision cannot happen.
Anatomy
graph TD
ROOT["my-plugin/"]
ROOT --> META[".claude-plugin/<br/>plugin.json<br/><b>ONLY this file goes here</b>"]
ROOT --> SK["skills/<br/>api-review/SKILL.md"]
ROOT --> AG["agents/<br/>log-triage.md"]
ROOT --> HK["hooks/<br/>hooks.json"]
ROOT --> MCP[".mcp.json<br/>MCP server definitions"]
ROOT --> BIN["bin/<br/>executables added to PATH"]
The single most common mistake is putting skills/, agents/, or hooks/
inside .claude-plugin/. Only plugin.json lives there. Everything else sits
at the plugin root.
plugin.json:
{
"name": "platform-tools",
"version": "1.2.0",
"description": "Shared review, release, and triage tooling for platform services."
}
A realistic plugin
What a platform team actually ships to twenty service repositories:
platform-tools/
├── .claude-plugin/
│ └── plugin.json
├── skills/
│ ├── api-review/SKILL.md # /platform-tools:api-review
│ ├── release-notes/SKILL.md
│ ├── incident/SKILL.md
│ └── deploy/SKILL.md # disable-model-invocation: true
├── agents/
│ ├── log-triage.md
│ ├── test-analyst.md
│ └── security-reviewer.md
├── hooks/
│ └── hooks.json # protect generated/, run the formatter
├── bin/
│ └── plat # internal CLI, added to PATH
└── .mcp.json # incident tracker, metrics backend
Each service repository then needs only:
{
"enabledPlugins": ["platform-tools@company-marketplace"]
}
instead of twenty copies of the same .claude/ tree drifting apart over a year.
Lifecycle
| Action | Command |
|---|---|
| Install | /plugin install platform-tools@company-marketplace |
| Test locally without installing | claude --plugin-dir ./my-plugin |
| Reload after editing | /reload-plugins |
| Enable per project | enabledPlugins in .claude/settings.json |
--plugin-dir is the development loop. Point it at a working copy, iterate, and
only publish once the components behave.
When to convert
Start with standalone configuration in .claude/ for quick iteration. Convert to
a plugin when you are ready to share. The practical trigger: a second
repository needs the same setup. Before that, packaging is overhead.
| Signal | Convert? |
|---|---|
| One repository, still changing weekly | No. Iterate in .claude/ |
A second repository copy-pasted your .claude/skills/ |
Yes, that copy will drift |
| Contributors keep hitting name collisions with personal skills | Yes, namespacing solves it |
| You need the same hooks enforced across an org | Yes, plus consider managed settings |
| You want version pinning and a rollback story | Yes |
Plugins contribute hooks you did not write. When debugging an unexpectedly blocked tool call, check the hooks from every enabled plugin, not just your own settings. See hooks.