Skip to content

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 standup skill in a plugin called team-rituals becomes /team-rituals:standup, so two plugins can both ship a standup without 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" }
}
FieldRequired?Notes
nameYesIdentifier and prefix for every skill and agent. No spaces
descriptionNoShown in /plugin
versionNoWhen set, users stay on that version until you bump it; see /docs/plugins/host-marketplace for when to set it or leave it out
authorNoname is required inside it; email and url are optional

Every other field is in /docs/plugins/manifest-reference.

Warning: Only plugin.json belongs 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-dev plugin from claude-plugins-official. It adds skills and agents for writing skills, hooks and MCP servers and for validating the result. Run /plugin-dev:create-plugin followed 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.

PathWhat goes there
.claude-plugin/plugin.jsonThe manifest. Without one, a plugin loaded via --plugin-dir takes its directory name
skills/<name>/SKILL.mdOne folder per skill
commands/*.mdFlat Markdown files, the older form of skills. Prefer skills/ for new work
agents/*.mdOne file per subagent
hooks/hooks.jsonA top-level "hooks" key, shaped exactly like hooks in a settings file
.mcp.jsonMCP 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.

MethodLastsGood for
--plugin-dir <path>One sessionDay-to-day development, testing a .zip build
--plugin-url <url>One sessionTrying a CI build artefact
CLAUDE_CODE_PLUGIN_DIRSSessions where it is setEnvironments where you cannot add a flag
claude plugin initEvery sessionPersonal 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:

  1. Send the folder or a .zip to a few people. Nothing to publish.
  2. List it in your own marketplace. Teammates add the marketplace once, install by name and receive updates.
  3. 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.

  1. Validate. claude plugin validate <path> checks the manifest and the frontmatter of every skill, agent and command, exiting 0 on Validation passed. Add --strict to fail on warnings as well.
  2. Reload. In the session, /reload-plugins applies on-disk edits and prints a Reloaded: line with counts. Type your /plugin-name:skill to confirm.
  3. Inspect. /plugin shows your plugin on Installed with the components it found, and Errors lists anything that failed and why.
  4. List from the shell. claude plugin list prints session-only and skills-directory plugins in their own sections with Status: ✔ loaded or the error. Include your dev plugin with claude --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

SymptomCauseFix
Errors tab shows <component> path not found: <path>A manifest path such as commands, skills, agents or hooks points at nothingFix 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.jsonPoint at one plugin's folder, or add the marketplace
Plugin loads but skills missingskills/ is inside .claude-plugin/, or a skills manifest entry points at a fileMove skills/ to the root; point entries at folders containing SKILL.md
userConfig dialog never appearsIt only appears when installing via /plugin in a session, not with --plugin-dir or claude plugin installRun /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.