Create a plugin
Build a Claude Code plugin from an empty folder, load it without a marketplace, debug it, and convert an existing .claude/ setup into one.
A plugin is a folder with a manifest (plugin.json) and any mix of skills, agents, hooks and MCP servers. Claude Code treats the folder as one unit, which is what makes it shareable. This page walks through building one from scratch, the ways to load it while you work, a debugging checklist, and how to turn an existing .claude/ directory into a plugin.
Installing someone else's plugin is covered in /docs/plugins/install. If you are unsure whether you need a plugin at all, read the decision section of /docs/plugins/overview first.
What changes when something moves into a plugin
Standalone skills, agents, hooks and MCP config work fine while they serve one person or one project. Once they move into a plugin, two things change:
- Location. Everything lives under the plugin's own directory, the plugin root:
skills/,agents/,hooks/hooks.json,.mcp.json. - Names. Skills and agents are prefixed with the plugin name. A
standupskill in a plugin calledteam-ritualsbecomes/team-rituals:standup, so two plugins can both ship astandupwithout clashing.
Build your first plugin
We will make a plugin called pr-notes with a single skill that drafts a pull request description. One skill is the smallest useful example; nothing is required, and you can add other components later.
You need Claude Code installed and signed in (see /docs/quickstart). Work from any folder; you pass the plugin's path to Claude Code, so it can live anywhere.
1. Make the folders
mkdir -p pr-notes/.claude-plugin pr-notes/skills/describe
2. Write the manifest
Save as pr-notes/.claude-plugin/plugin.json:
{
"name": "pr-notes",
"description": "Drafts pull request descriptions from the current branch",
"version": "0.1.0",
"author": { "name": "Cameron Shields", "url": "https://cameronshields.co.uk" }
}
| Field | Required? | Notes |
|---|---|---|
name | Yes | Identifier and prefix for every skill and agent. No spaces |
description | No | Shown in /plugin |
version | No | When set, users stay on that version until you bump it; see /docs/plugins/host-marketplace for when to set it or leave it out |
author | No | name is required inside it; email and url are optional |
Every other field is in /docs/plugins/manifest-reference.
Warning: Only
plugin.jsonbelongs inside.claude-plugin/. Skills, agents and hooks saved in there are ignored. They go in the plugin root next to it.
3. Add the skill
Save as pr-notes/skills/describe/SKILL.md:
---
name: describe
description: Write a pull request description for the current branch
disable-model-invocation: true
---
Run `git log main..HEAD --oneline` and `git diff main...HEAD --stat`.
Write a PR description with three headings: Why, What changed, How to test.
Keep it under 200 words. Do not invent test steps you cannot see evidence for.
disable-model-invocation: true means only you can trigger it; Claude will not run it on its own initiative. Remove that line for skills you want Claude to pick up automatically. The full frontmatter list is on /docs/skills.
4. Validate
claude plugin validate ./pr-notes
You should see the manifest path it checked and ✔ Validation passed. If you get ✘ Validation failed, the lines above it name the field to fix; /docs/plugins/troubleshooting explains each message.
5. Run it
claude --plugin-dir ./pr-notes
Then in the session:
/pr-notes:describe
That plugin only exists in sessions started with --plugin-dir. The next section covers other ways to load it.
Tip: For bigger plugins, install Anthropic's
plugin-devplugin fromclaude-plugins-official. It adds skills and agents for writing skills, hooks and MCP servers and for validating the result. Run/plugin-dev:create-pluginfollowed by a description of what you want, and Claude walks you through design, creation and validation.
Plugin layout
Each component type has a fixed home under the plugin root. Create only the ones you use.
| Path | What goes there |
|---|---|
.claude-plugin/plugin.json | The manifest. Without one, a plugin loaded via --plugin-dir takes its directory name |
skills/<name>/SKILL.md | One folder per skill |
commands/*.md | Flat Markdown files, the older form of skills. Prefer skills/ for new work |
agents/*.md | One file per subagent |
hooks/hooks.json | A top-level "hooks" key, shaped exactly like hooks in a settings file |
.mcp.json | MCP server definitions |
The plugin root is the plugin's own folder, never ~/.claude/. A .mcp.json dropped at ~/.claude/.mcp.json will not load as plugin config. The complete layout is in /docs/plugins/manifest-reference, and /docs/plugins/components explains each component.
Load a plugin without a marketplace
While developing, you never need a marketplace. Pick whichever loading method fits.
| Method | Lasts | Good for |
|---|---|---|
--plugin-dir <path> | One session | Day-to-day development, testing a .zip build |
--plugin-url <url> | One session | Trying a CI build artefact |
CLAUDE_CODE_PLUGIN_DIRS | Sessions where it is set | Environments where you cannot add a flag |
claude plugin init | Every session | Personal plugins you always want loaded |
None of the one-session methods write anything to your settings. When you edit files mid-session, run /reload-plugins. If two plugins loaded by different routes share a name, /docs/plugins/loading explains which wins.
--plugin-dir
Pass a plugin root or a .zip of one. Repeat it for several:
claude --plugin-dir ./pr-notes --plugin-dir ~/builds/release-kit.zip
A folder of plugins (v2.1.265+). You can also pass a folder containing several plugins, for example --plugin-dir ./plugins. If that folder has no .claude-plugin/ and no components at its top level, each immediate subfolder with a .claude-plugin/plugin.json loads as its own plugin. Anything else, including subfolders without a manifest, is skipped silently. If one does not appear, check its manifest exists.
From v2.1.281 the folder may also hold a .claude-plugin/marketplace.json beside the plugin folders, as long as that .claude-plugin/ contains no plugin.json. Claude Code does not read the marketplace file, so nothing is installed from it; the subfolders just load.
In an interactive session a plugins folder is live: a new subfolder loads once its manifest exists, and deleting a subfolder unloads it. You get a message for each change. If loading or unloading would invalidate the prompt cache, the change waits and the message tells you to run /reload-plugins.
--plugin-url
claude --plugin-url https://ci.example.com/artifacts/pr-notes-0.2.0.zip
The archive downloads at startup. Repeat the flag, or put several space-separated URLs in one quoted argument. Only use archives you control or trust. If the download fails or the archive is invalid, Claude Code starts without it and logs a load error on the Errors tab of /plugin.
CLAUDE_CODE_PLUGIN_DIRS
List absolute paths in this environment variable (v2.1.280+) and each loads exactly as a --plugin-dir would, in addition to any flags. Project and local settings files cannot set it. Managed settings can switch off both this variable and --plugin-dir; see /docs/plugins/cli-reference. For testing a plugin alongside one it depends on, see /docs/plugins/dependencies.
claude plugin init: always loaded
Any folder under ~/.claude/skills/ that contains .claude-plugin/plugin.json loads as a plugin in every session with no install. claude plugin init scaffolds one:
claude plugin init standup
This creates ~/.claude/skills/standup/ with a manifest and a root SKILL.md, then prints ✔ Created plugin "standup" at ~/.claude/skills/standup and tells you it will auto-load next session as standup@skills-dir (or run /reload-plugins now). Add --with skills to also scaffold a skill under skills/; other --with values are in /docs/plugins/cli-reference.
Naming quirk: the root SKILL.md is also a personal skill, so it runs as /standup, not /standup:standup. Skills under the plugin's skills/ folder get the prefix, such as /standup:weekly.
To stop loading it, delete the folder or run claude plugin disable standup@skills-dir. The skills-dir part sits where a marketplace name normally goes.
To share one through a repository instead, build the same layout by hand at <project>/.claude/skills/<name>/ with its own .claude-plugin/plugin.json. /docs/plugins/loading lists when Claude Code loads these.
Share it
Three routes, from lightest to heaviest:
- Send the folder or a
.zipto a few people. Nothing to publish. - List it in your own marketplace. Teammates add the marketplace once, install by name and receive updates.
- Submit it to Anthropic's directory. After review, people add it on claude.ai or in Cowork and it reaches Claude Code through account sync.
All three are covered in /docs/plugins/publish.
Test and debug
When a change does not show up, work down this list. Each step tells you what Claude Code actually did with your plugin.
- Validate.
claude plugin validate <path>checks the manifest and the frontmatter of every skill, agent and command, exiting0onValidation passed. Add--strictto fail on warnings as well. - Reload. In the session,
/reload-pluginsapplies on-disk edits and prints aReloaded:line with counts. Type your/plugin-name:skillto confirm. - Inspect.
/pluginshows your plugin on Installed with the components it found, and Errors lists anything that failed and why. - List from the shell.
claude plugin listprints session-only and skills-directory plugins in their own sections withStatus: ✔ loadedor the error. Include your dev plugin withclaude --plugin-dir ./pr-notes plugin list.
For MCP servers, run /mcp; a healthy server shows as connected. For hooks, trigger the event (ask Claude to edit a file to fire a PostToolUse hook) and read the debug log, which shows matches, exit codes and output. See /docs/hooks.
Common development failures
| Symptom | Cause | Fix |
|---|---|---|
Errors tab shows <component> path not found: <path> | A manifest path such as commands, skills, agents or hooks points at nothing | Fix the path or create the folder, then /reload-plugins |
--plugin-dir at a marketplace root loads nothing, no error | --plugin-dir wants a plugin root; it does not read marketplace.json | Point at one plugin's folder, or add the marketplace |
| Plugin loads but skills missing | skills/ is inside .claude-plugin/, or a skills manifest entry points at a file | Move skills/ to the root; point entries at folders containing SKILL.md |
userConfig dialog never appears | It only appears when installing via /plugin in a session, not with --plugin-dir or claude plugin install | Run /plugin configure <plugin-name> with the plugin loaded |
Does it actually change Claude's behaviour?
Loading cleanly is not the same as working. claude plugin eval runs your test cases with and without the plugin and scores the difference. Start with /docs/plugin-evals.
Convert an existing .claude/ setup
If you already have skills, agents or hooks in a project's .claude/, you can lift them into a plugin without rewriting. Run these from the project root.
1. Create the plugin.
mkdir -p acme-dev/.claude-plugin
cat > acme-dev/.claude-plugin/plugin.json <<'EOF'
{
"name": "acme-dev",
"description": "Acme's shared Claude Code setup",
"version": "1.0.0"
}
EOF
2. Copy whichever folders you have.
for d in commands agents skills; do
[ -d ".claude/$d" ] && cp -r ".claude/$d" acme-dev/
done
ls -a acme-dev
3. Move hooks. If .claude/settings.json or .claude/settings.local.json has a hooks object, copy it into acme-dev/hooks/hooks.json under a top-level "hooks" key. The shape is identical:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "npx prettier --write \"$(jq -r '.tool_input.file_path')\"" }
]
}
]
}
}
4. Test under the new names.
claude --plugin-dir ./acme-dev
A skill that was /release is now /acme-dev:release. An agent that was migrator is now acme-dev:migrator. Trigger each hook's event to check it fires.
While the originals still exist in .claude/, both copies load. Skills and agents coexist happily thanks to the prefix (/release and /acme-dev:release both work). Hooks have no prefix, so a hook in both places runs twice per event. Once the plugin works, delete the originals and remove the hooks object from your settings.