Claude Code Plugin Tutorial: Build One and Share It
A Claude Code plugin is a directory with a .claude-plugin/plugin.json manifest and whatever components you want to ship next to it: skills, agents, hooks, MCP servers, LSP servers, background monitors. You test it with claude --plugin-dir ./my-plugin, and you distribute it by listing it in a .claude-plugin/marketplace.json that other people add with /plugin marketplace add.
That is the whole system. The rest of this article is the details that decide whether your plugin loads on someone else's machine on the first try.
Verified against the Claude Code plugin documentation at code.claude.com on 2026-08-21.
Plugin or plain .claude/?
Do not reach for a plugin first. Claude Code already loads skills, agents, and hooks from a .claude/ directory in your project or home folder, and that path has a shorter feedback loop: edit the file, use it.
The difference that matters is distribution and naming:
Standalone .claude/ | Plugin | |
|---|---|---|
| Invocation | /hello | /plugin-name:hello |
| Sharing | copy files by hand | /plugin install from a marketplace |
| Versioning | none | version in the manifest |
| Scope | one project (or your user config) | any project, any teammate |
So: build in .claude/ while you are figuring out what the thing should do, then convert it to a plugin when a second person needs it. The official docs recommend the same order, and converting later is mostly a cp.
If you have not written a skill yet, start with our Claude Code skills tutorial; the plugin is just a wrapper around skills you already know how to write.
The minimum viable plugin
Three files. Here is the layout, using a plugin that drafts release notes:
release-notes-plugin/
├── .claude-plugin/
│ └── plugin.json
└── skills/
└── release-notes/
└── SKILL.md
.claude-plugin/plugin.json:
{
"name": "release-notes-plugin",
"description": "Drafts release notes from git history",
"version": "1.0.0",
"author": { "name": "Your Name" },
"license": "MIT"
}
Only three of those do real work. name is the identifier and the namespace prefix for every skill in the plugin, so skills/release-notes/ becomes /release-notes-plugin:release-notes. description is what shows in the plugin manager. version is optional, and it has a consequence covered below. author, license, homepage, and repository are metadata.
skills/release-notes/SKILL.md:
---
description: Draft release notes from git history. Use when preparing a release, tagging a version, or writing a changelog entry.
---
Run `git log --oneline $ARGUMENTS..HEAD` and group the commits under Added,
Changed, and Fixed. Skip merge commits and dependency bumps. Write one line
per change in plain language, not the commit subject verbatim.
The folder name is the skill name. $ARGUMENTS captures whatever the user types after the skill invocation, so /release-notes-plugin:release-notes v1.4.0 passes v1.4.0 straight into that git log range.
Write the description for a reader who is not you. Claude decides whether to invoke a skill from that line alone, so "Use when preparing a release, tagging a version, or writing a changelog entry" earns its length. A description that just restates the name gets ignored.
Load it:
claude --plugin-dir ./release-notes-plugin
Then run /release-notes-plugin:release-notes inside the session. --plugin-dir takes a .zip archive too, and you can repeat the flag to load several plugins at once. When a --plugin-dir plugin shares a name with one you have installed from a marketplace, the local copy wins for that session, which is how you test a change to a plugin you already use.
While you iterate, /reload-plugins picks up edits without a restart.
The mistake that eats an afternoon
Only plugin.json goes inside .claude-plugin/. Everything else lives at the plugin root, next to that directory, not inside it.
my-plugin/
├── .claude-plugin/
│ └── plugin.json ← only this
├── skills/ ← root level
├── agents/ ← root level
├── hooks/
│ └── hooks.json
├── .mcp.json
├── .lsp.json
├── monitors/
│ └── monitors.json
├── bin/
└── settings.json
Put skills/ inside .claude-plugin/ and the plugin loads with nothing in it, no error, no hint. This is called out as the common mistake in Anthropic's own docs, which tells you how often it happens.
The plugin root is the individual plugin's directory: the one you pass to --plugin-dir, or the one containing .claude-plugin/plugin.json. It is never ~/.claude/.
What else you can put in there
Each of those directories is worth knowing about before you decide a plugin is overkill:
skills/andcommands/: skills as<name>/SKILL.mdfolders, or as flat Markdown files. Useskills/for anything new.agents/: custom subagent definitions, the same format covered in our subagents guide. Note that a project or user.claude/agents/definition with the same name overrides the plugin's, so remove the originals when you migrate.hooks/hooks.json: the samehooksobject you would otherwise put insettings.json, moved into the plugin verbatim. Our hooks guide covers the event model..mcp.json: MCP server configs, which is how the officialgithub,linear, andsentryplugins work. The plugin is the delivery mechanism, the MCP server is the capability..lsp.json: language server config, giving Claude real diagnostics after every edit rather than grep. Anthropic ships pre-built LSP plugins for TypeScript, Python, Rust, Go, and others, so write your own only for a language they have not covered.monitors/monitors.json: background commands whose stdout lines arrive as notifications during the session. Atail -F ./logs/error.logentry means Claude sees errors as they happen without being asked to look.bin/: executables added to the Bash tool'sPATHwhile the plugin is enabled.settings.json: defaults applied when the plugin is on. OnlyagentandsubagentStatusLineare read today. Settingagentpromotes one of the plugin's own agents to the main thread, so a single plugin can change the personality of the whole session.
A plugin shipping exactly one skill can skip the skills/ directory and put SKILL.md at the plugin root. Use the directory anyway if there is any chance of a second skill.
Distributing it: the marketplace file
A marketplace is one JSON file that lists plugins and where to fetch each one. It lives at .claude-plugin/marketplace.json in the repository root.
{
"name": "skillforge-demo",
"owner": { "name": "Your Name", "email": "[email protected]" },
"plugins": [
{
"name": "release-notes-plugin",
"source": "./release-notes-plugin",
"description": "Drafts release notes from git history"
}
]
}
name, owner, and plugins are the required fields. Each plugin entry needs name and source at minimum.
Two things about name that bite later. First, it is public: users type /plugin install release-notes-plugin@skillforge-demo, so the marketplace name is part of your install instructions forever. Second, each user can register only one marketplace per name, and adding a second with the same name replaces the first. If you want to publish several plugins, list them all in one marketplace.json rather than spinning up a marketplace per plugin.
A set of names is reserved for Anthropic (claude-plugins-official, claude-community, anthropic-plugins, and others), along with names that imitate them. Claude Code re-checks this on every load, not just at add time.
Source types
source is either a relative path string or an object:
| Source | Fields | Use it when |
|---|---|---|
"./my-plugin" | none | the plugin lives in the marketplace repo |
github | repo, ref?, sha? | the plugin has its own GitHub repo |
url | url, ref?, sha? | GitLab, Bitbucket, self-hosted git |
git-subdir | url, path, ref?, sha? | a plugin inside a monorepo (clones sparsely) |
npm | package, version?, registry? | you already publish to npm |
archive | url, sha256? | users may not have git or npm |
command | command, timeout?, mode? | the plugin is generated locally |
Relative paths must start with ./ and resolve against the marketplace root, the directory containing .claude-plugin/, not against the JSON file itself. So "./plugins/formatter" means <repo>/plugins/formatter even though the manifest sits one level down. Do not use ../.
One trap worth knowing before you pick: relative paths do not work if users add your marketplace by direct URL to the marketplace.json file, because Claude Code downloads only that one file and has nothing to resolve against. If you plan to distribute by URL rather than by git repo, use any other source type.
When both ref and sha are set on a git source, the sha wins and Claude Code checks out that commit directly.
Installing and scoping
/plugin marketplace add ./my-marketplace
/plugin install release-notes-plugin@skillforge-demo
The marketplace can also come from owner/repo shorthand for GitHub, a full git URL for other hosts, a local path, or a URL to a hosted marketplace.json. Append #branch-or-tag to pin a ref.
The install command opens a details view where you pick a scope:
- User: you, everywhere.
- Project: everyone on this repository. Writes to
.claude/settings.json. - Local: you, this repository only. Not shared.
Check what the install summary says. Plugin is now active. means you are done. Run /reload-plugins to activate. means run it. If the reload would invalidate the prompt cache, it warns and skips until you rerun with --force.
For scripting, the shell commands (claude plugin install, claude plugin uninstall, both accepting --scope) do the same thing without opening the interactive panel. Run claude plugin validate ./your-plugin before you publish; the community-marketplace review pipeline runs the same check.
Rolling it out to a team
Put the marketplace in the repository's .claude/settings.json and every teammate who trusts the folder gets it without a prompt:
{
"extraKnownMarketplaces": {
"my-team-tools": {
"source": {
"source": "github",
"repo": "your-org/claude-plugins"
}
}
}
}
Adding the marketplace is not the same as installing the plugins. As of v2.1.195, a plugin from an external source that only the project's settings enable does not load until the teammate installs it; Claude Code reports it as not installed and prints the claude plugin install command to run. Plugins stored inside the marketplace repo and referenced by relative path avoid that extra step.
Admins can also set "autoUpdate": true on an extraKnownMarketplaces entry so the whole org gets refreshes without each person toggling it.
Versioning, and the reason your update never shipped
If version is set, in either plugin.json or the marketplace entry, the plugin is pinned to that string and users only get an update when you change it. Push a fix without bumping version and nobody sees it. This is the single most common "my plugin update did not reach anyone" cause.
Omit version entirely and Claude Code falls back to other signals instead, which is reasonable for a plugin you are iterating on daily inside your own team. Set it, and bump it, for anything public.
Small things that look like bugs
/reload-pluginsreports0 skills. The skills count covers only thecommands/directory. Yourskills/folder reloaded fine. Test the skill rather than trusting the number.- A migrated agent has no effect. Project and user
.claude/agents/definitions override same-named plugin agents. Delete the original. - A migrated skill appears twice. Plugin skills are namespaced, so
/skill-nameand/plugin-name:skill-namecoexist rather than one shadowing the other. Remove the.claude/copy after migrating. - Skills do not appear at all. Clear the cache with
rm -rf ~/.claude/plugins/cache, restart, reinstall. - An LSP server is missing. The plugin configures the language server, it does not install the binary. Check the
/pluginErrors tab forExecutable not found in $PATH. - Copied plugins cannot read
../shared-utils. Install copies the plugin directory into~/.claude/plugins/cache, so anything outside it is simply not there. Use symlinks if you must share files across plugins.
Before you install someone else's
Plugins execute arbitrary code with your user privileges. Hooks run shell commands, bin/ lands executables on your PATH, MCP servers talk to the network, and none of it is sandboxed away from your machine. Anthropic does not control what a third-party plugin contains and does not verify that it works as intended.
Read the source of anything you install from outside your own organization, the same way you would read a shell script before piping it into bash. Plugins in the claude-community marketplace pass automated validation and safety screening and are pinned to a specific commit SHA, which raises the floor but is not a code review. Organizations can restrict which marketplaces are allowed at all through managed settings.
FAQ
Do I need a marketplace to use a plugin? No. claude --plugin-dir ./my-plugin loads it for that session, and claude plugin init my-tool scaffolds one under ~/.claude/skills/ that loads automatically as my-tool@skills-dir with no marketplace at all. A marketplace is for handing the plugin to other people.
Where does a plugin actually get installed? Claude Code copies it into a versioned cache at ~/.claude/plugins/cache. The exception is a command source in link mode, which is used in place.
How do I publish to the community marketplace? Submit through the in-app form at platform.claude.com/plugins/submit (or the claude.ai equivalent, which requires a Team or Enterprise org with directory management access). Run claude plugin validate first; the review pipeline runs the same check plus automated safety screening. Approved plugins are pinned to a commit SHA and the public catalog syncs nightly, so expect a delay between approval and the plugin becoming installable.
Can I get into the official claude-plugins-official marketplace? Not by applying. It is curated by Anthropic at its discretion, and the submission form feeds the community marketplace instead.
What is the difference between a marketplace source and a plugin source? The marketplace source is where the marketplace.json catalog comes from, set when a user runs /plugin marketplace add. The plugin source is where each individual plugin comes from, set inside the catalog. They are pinned independently, and git-based marketplace sources support ref but not sha.
Does a plugin cost me context on every turn? Yes, and the /plugin details pane shows a context cost estimate before you install. That is the argument for uninstalling what you stopped using; the Installed tab groups those under a "Not used recently" header.
Can a plugin change how Claude behaves overall, not just add commands? Yes. A plugin root settings.json with {"agent": "security-reviewer"} promotes one of the plugin's own agents to the main thread, applying its system prompt, tool restrictions, and model to the whole session.