Skip to content

Plugin Reference Manual: Pack Your Configurations into a Shareable Package

📚 Series Navigation: The previous article 37 Checkpoints taught you how to "save and load" sessions, allowing you to return to the last safe point with one click if something breaks. This article shifts to another level—Article 24 taught you "how to use other people's plugins", this article teaches you "how to build your own plugins": how to structure the plugin directories, what each field in plugin.json manages, what components can be packaged, how to write dependencies, and how to build a marketplace to share it with your team. This is the deep "reference manual" for plugins.

If you run claude plugin details on a plugin, there is often a line of numbers worth noting in the output: this plugin takes up ~180 tokens persistently per session, and its two skills eat up another ~2400 and ~1800 tokens respectively when triggered.

Even such an innocent-looking little plugin takes up a chunk of the workbench just by "hanging there doing nothing". You'll realize then: a plugin is not a black box—it is pieced together from several clear types of components, and how much each type takes, where it's placed, and how it's triggered are all well-documented. Only by thoroughly understanding this structure can you talk about "building one yourself, and building it cleanly".

In Article 24, we've already walked through "using other people's plugins": adding marketplaces, installing plugins, /reload-plugins to take effect, checking trust before installation. This article won't repeat those, but will bite into a harder piece—the internal structure, development, and publishing of plugins. Frankly speaking, Article 24 is about "knowing how to drive", while this article is about "knowing how to dismantle the engine, and even build a car yourself".

This article leans towards a "reference manual", so the information density will be higher than the previous ones. You don't need to memorize it all at once. First, follow along to build a runnable plugin and publish it, and use the remaining field tables as a dictionary to check when needed.

After reading this article, you will get:

  • The standard directory appearance of a plugin, and the complete structure behind the iron rule "only plugin.json goes in .claude-plugin/".
  • A panoramic view of plugin.json manifest fields: required ones, metadata, component paths, user configurations, dependencies, all in one table to check at a glance.
  • All components a plugin can package (skill / command / agent / hook / MCP / LSP / monitor), where to place them, and what limitations they have.
  • Why path variables like ${CLAUDE_PLUGIN_ROOT} must be used, and what happens if you don't.
  • The complete chain from building a plugin from scratch, local testing, creating a marketplace, to distributing it to a team, with commands and expected outputs throughout.
  • How to fill the two unavoidable pitfalls during publishing: version management and dependencies.

01 First Get the "Skeleton" Right: Standard Directory Appearance

In Article 24, you've seen the simplest form of a plugin—a plugin.json plus a few component folders. But to build a decent plugin yourself, you must first see the complete skeleton clearly, otherwise it will get messy halfway through.

Let's give the conclusion first: A plugin is just a folder, containing a "manifest file" to determine its identity, and the rest are placed in the root directory categorized by component type.

Analogy: A set of LEGO bricks. A box of LEGOs contains two things—an assembly instruction manual, telling you what this set is called and what parts are in it; and several divided parts boxes, sorting the bricks by type (wheels in one grid, windows in another). A plugin is exactly this structure: plugin.json is that instruction manual, while folders like skills/, agents/, hooks/ are the divided parts boxes. The instruction manual has its exclusive place, while the parts boxes are spread out outside—this is where the iron rule below comes from.

The complete plugin directory provided officially looks like this (I deleted the uncommonly used ones and kept the core):

text
my-plugin/
├── .claude-plugin/           # Metadata directory
│   └── plugin.json           # Manifest (instruction manual) — only this goes here
├── skills/                   # Skills, each as <name>/SKILL.md
│   └── code-reviewer/
│       └── SKILL.md
├── commands/                 # Flat .md syntax for Skills (older format)
│   └── status.md
├── agents/                   # Subagent definitions
│   └── security-reviewer.md
├── hooks/                    # Hook configurations
│   └── hooks.json
├── .mcp.json                 # MCP server definitions
├── .lsp.json                 # LSP server configurations
├── bin/                      # Executables added to PATH
├── scripts/                  # Scripts for hooks and tools
└── settings.json             # Default settings for the plugin

There is an iron rule circled with a warning box by the official docs, which beginners are bound to step into, I'll nail it down for you:

The .claude-plugin/ directory contains the plugin.json file. All other directories (commands/, agents/, skills/, output-styles/, themes/, monitors/, hooks/) must be in the plugin root, not inside .claude-plugin/.

To put it plainly: Only the plugin.json file is allowed to lie in this .claude-plugin/ folder, skills/, agents/, hooks/ are all placed outside it, on the same level as it. This pitfall was mentioned in Article 24—the first time packing, it's easy to accidentally stuff skills/ into .claude-plugin/. As a result, the plugin loads, but the skill absolutely won't appear, and after troubleshooting for half a day, you find out it was placed wrong. Remember that LEGO image: the instruction manual belongs in its own box, the parts boxes are all placed outside.

There's also an easily overlooked point, clearly stated officially: A CLAUDE.md in the plugin's root directory will not be loaded as project context. If a plugin wants to feed instructions to Claude, it must go through components like skill, agent, hook, and cannot rely on stuffing a CLAUDE.md in the plugin. This is a completely different matter from the project-level CLAUDE.md discussed in Article 18, don't confuse them.

💡 One-sentence summary: Plugin = an instruction manual (.claude-plugin/plugin.json) + divided parts boxes (component folders) in the root directory; there is only one iron rule—only plugin.json goes in .claude-plugin/, all other folders are placed in the root directory.


02 plugin.json: What Each Field in this "Manifest" Manages

With the skeleton set straight, let's dismantle that instruction manual—plugin.json. It declares the plugin's identity and configuration, and is the core of the entire plugin.

First, note a counter-intuitive but very reassuring fact: the manifest is optional. Official original words—if you omit plugin.json, Claude Code will automatically look for components in standard locations (like skills/, agents/), and even deduce the plugin name from the folder name. You only need this manifest when you need to write metadata, or customize component paths. However, if you want to publish it properly, you'll definitely have to write it, so we'll talk as if you're writing it.

Analogy: A customs declaration form. When a batch of goods goes through customs, a declaration form must be attached—stating clearly what the goods are called, who sent them, the version batch, and what items are packed. Customs (Claude Code) checks, registers, and shelves according to this form. plugin.json is the plugin's customs declaration form: name, author, version, and what components are packed, are all clearly stated on this one piece of paper.

The Only Required Field

If you write a manifest, only one field is required:

FieldTypeDescription
namestringUnique identifier, kebab-case (lowercase with hyphens), no spaces

Why is name so critical? Because it is the namespace prefix for components. An agent called agent-creator inside a plugin named plugin-dev will be displayed as plugin-dev:agent-creator in the interface; calling a skill is /plugin-dev:xxx. Namespacing is the fundamental mechanism to prevent plugin name collisions—if you install ten plugins, each skill carries its own prefix and won't fight.

Metadata Fields (Describing "What" the Plugin Is)

These don't affect functionality, but should be filled when publishing so users understand:

FieldWhat it does
displayNameHuman-readable name displayed in the interface, can have spaces and mixed cases; if omitted, falls back to name
versionSemantic versioning. If set, users only receive updates when you bump the version number (This is a major pitfall, detailed in Section 08)
descriptionA one-sentence explanation of what the plugin does, shown during browsing/installation
authorAuthor info (name / email / url)
homepage / repository / licenseDocs URL / Source URL / License
keywordsDiscovery tags, helps people search for you

Component Path Fields (Pointing Out "Where" Components Are)

By default, components are just placed in standard locations like skills/, agents/, you can completely skip writing these fields. They are only used when you want to put components in non-standard paths:

FieldPoints to what
skillsExtra skill directories (appended to default skills/)
commands / agents / outputStylesCustom paths (replaces default directories)
hooks / mcpServers / lspServersConfig file paths, or written inline right here
dependenciesOther plugins this plugin depends on (Discussed in Section 08)

There is a detail here that is very easy to trip over, the official docs specifically list "path behavior rules": some fields "replace defaults", some "append to defaults".

  • Replaces default: commands, agents, outputStyles. Once you write commands, the default commands/ directory will no longer be scanned. To keep the defaults while adding others, you must list it explicitly: "commands": ["./commands/", "./extras/"].
  • Appends to default: skills. The default skills/ will always be scanned, directories you list in the skills field are loaded alongside it.

This pitfall is easy to step on: adding "agents": ["./extra-agents/reviewer.md"] to a plugin, thinking "I'll just add another agent", only to find the two agents originally in the agents/ directory are completely gone—because agents is replace not append. Changing to listing all three in the array works. If you can't remember, check this rule, don't guess.

A complete manifest with metadata looks something like this:

json
{
  "name": "deployment-tools",
  "displayName": "Deployment Tools",
  "version": "1.2.0",
  "description": "Deployment automation tools",
  "author": { "name": "Dev Team", "email": "dev@company.com" },
  "license": "MIT",
  "keywords": ["deployment", "ci-cd"]
}

💡 One-sentence summary: plugin.json is the plugin's customs declaration form, the only required one is name (it sets the namespace); in component path fields, distinguish between "replaces default" (commands/agents) and "appends to default" (skills), getting them reversed will make components disappear out of thin air.


03 What Components Can a Plugin Package: Recognize the Seven Types of Parts

This is the section you should memorize most in this article—what exactly are the seven types of parts that can fit into this plugin box. Article 24 briefly touched on a few; here we list all the officially supported ones clearly, marking where each goes and what unique limitations they have.

Analogy: Those grids in the LEGO parts box, each grid only holds one type of piece. Wheels grid, windows grid, minifigure grid—categorizing them makes assembly fast. Plugin components are also one grid, one type:

ComponentWhere it goesOne-sentence roleHow to trigger
Skillsskills/<name>/SKILL.mdCallable specialized abilities (Article 26)You type /pluginName:skillName, or Claude calls it automatically
Commandscommands/*.mdFlat old syntax for skills, new plugins use skillsSame as above
Agentsagents/*.mdSpecialized subagent (Article 23)Appears in /agents, Claude dispatches or you click
Hookshooks/hooks.jsonEvent-triggered automated actions (Article 33)Automatically triggered by lifecycle events
MCP servers.mcp.jsonConnects to external services (Article 22)Automatically starts when enabled, tools mixed into toolbox
LSP servers.lsp.jsonReal-time code intelligence (jump to definition, find references)Automatically used when processing code, requires installing language server separately
Monitorsmonitors/monitors.jsonMonitors logs/status in background, notifies Claude on activityAutomatically starts when plugin is activated (experimental)

A few categories need specific highlighting, as they are limitations stated in the official docs but easily missed:

Agents are "stripped of power" inside plugins. This is critical. The official docs are clear: For security reasons, agents provided by plugins do not support the hooks, mcpServers, and permissionMode frontmatter fields. That is to say, a subagent inside a plugin cannot secretly hook events, start an MCP, or change permission modes on its own—this prevents you from installing a plugin whose agent changes your permissions behind your back. The fields it supports include name, description, model, effort, maxTurns, tools, disallowedTools, skills, memory, background, isolation (the only valid value for isolation is "worktree").

Hooks can listen to a ton of events. Plugin hooks listen to the same batch of lifecycle events as the hooks you write yourself (Article 33)—from SessionStart (session starts), PreToolUse (before a tool is called, can be intercepted), PostToolUse (after successful call), to Stop (answer finishes), SessionEnd (session terminates), the official docs list thirty of them. Beginners don't need to memorize them all, just remember "you can attach an action at almost any timing", we'll leave the specifics for Article 33.

Hook types are more than "run script". Besides the most common command (run a shell command), there are http (send event to a URL), mcp_tool (call an MCP tool), prompt (evaluate a prompt with a model), agent (run an agentic validator).

Monitors are experimental. It lets the plugin keep an eye on logs or status in the background, treating new lines as notifications to feed to Claude, without you having to ask it to monitor. Experimental, architecture might change, and only runs in interactive sessions, requiring Claude Code v2.1.105 or above. Beginners just need to know such a thing exists.

There are also two "semi-components" worth knowing: Executables in the bin/ directory are added to Bash tools' PATH when the plugin is enabled, and can be called directly as raw commands; settings.json is the default settings for the plugin, but currently only supports the agent and subagentStatusLine keys—setting agent lets the plugin push a custom agent to the main thread upon enabling, essentially "installing this plugin changes the persona".

💡 One-sentence summary: Plugins can pack seven types of parts—skill / command / agent / hook / MCP / LSP / monitor, each with fixed locations; focus on remembering two limits: agents in plugins are not allowed to have hook/MCP/permission mode (security power strip), monitors are experimental.


04 ${CLAUDE_PLUGIN_ROOT}: Why Paths Must Use Variables and Not Be Hardcoded

This section is singled out because it is the most frequent source of errors when building plugins yourself, and the reasoning can be explained in one sentence.

First look at the scenario: your plugin has a hook to run scripts/format.sh, or an MCP server needs to node server.js. You naturally write the absolute path /Users/You/my-plugin/scripts/format.shdisaster, this path simply does not exist on someone else's machine, and even on your machine, the plugin's cached directory changes every time it updates.

The official solution gives you three path variables which are automatically replaced in hook commands, MCP/LSP configs, and skill/agent content:

VariablePoints toUsed to
${CLAUDE_PLUGIN_ROOT}Absolute path of the plugin's installation directoryReference scripts, binaries, configs bundled with the plugin
${CLAUDE_PLUGIN_DATA}Plugin's persistent data directory (kept after updates)Put node_modules, caches, state to be kept across versions
${CLAUDE_PROJECT_DIR}Project root directoryReference scripts/configs within the project

Analogy: Writing "location in this binder" in a loose-leaf binder, instead of copying dead page numbers. Imagine a loose-leaf binder that gets repeatedly rebound, and you have to write "go to the appendix page" in it. If you hardcode "page 87", once rebound, all page numbers are messed up; the smart way is to write "the appendix of this binder", a placeholder phrasing relative to the binder itself. ${CLAUDE_PLUGIN_ROOT} is this "store address" placeholder—no matter where the plugin is installed or updated to what version, it always points to the root directory of this current version.

So referencing a bundled script within a plugin, the standard syntax looks like this (note the double quotes, to prevent paths with spaces):

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"${CLAUDE_PLUGIN_ROOT}\"/scripts/format-code.sh"
          }
        ]
      }
    ]
  }
}

There is a hard rule repeatedly emphasized officially here, worth sticking on your forehead:

This path will change when the plugin is updated. The previous version's directory is kept on disk for about 7 days after an update for cleanup purposes, but should be considered temporary—do not write state here.

Translation: ${CLAUDE_PLUGIN_ROOT} is temporary, do not write things into it that need to be kept long-term (like installed dependencies, caches)—those should be written into ${CLAUDE_PLUGIN_DATA}, because the data directory is preserved across versions. A common crash is putting node_modules installed by the plugin into ROOT; when the plugin updates, the dependencies are all gone and must be reinstalled. Use ROOT for bundled scripts, use DATA for state to be kept, distinguishing these two will keep you out of trouble.

There's also a related pitfall, which the official docs specifically cover in the "Plugin Caching" section: Installed plugins cannot reference files outside their own directory. Writing an outward-jumping relative path like ../shared-utils will fail after installation—because when a plugin is installed, it is copied entirely into the cache (~/.claude/plugins/cache), and files outside are not copied over at all. To share files across plugins, you have to use symlinks, which is an advanced topic; beginners should first remember "don't reference outside the plugin".

💡 One-sentence summary: Path references within a plugin must use variables like ${CLAUDE_PLUGIN_ROOT}, cannot hardcode absolute paths (changes every update); use ROOT for bundled scripts, DATA for state to be kept, and never reference files outside the plugin directory.


05 Hands-on: Build a Runnable Plugin from Scratch, Then Test Locally

Having talked through construction for four sections, it's time to actually get our hands dirty. Next, I will lead you to personally build a minimal but complete plugin, load it locally, and run its skill—without relying on any complex environment, just follow along and copy. We'll build a my-greeter plugin, with a skill for saying hello.

Step 1: Use the official scaffold command to start a plugin skeleton

The easiest way is not to manually create directories, but to use the official plugin init command, which will set up the skeleton:

bash
claude plugin init my-greeter --with skills

--with skills means to incidentally set up an example skill folder. This command will create .claude-plugin/plugin.json and a starting SKILL.md under ~/.claude/skills/my-greeter/.

Expectation: The terminal prompts that the plugin skeleton is created, telling you it's built at ~/.claude/skills/my-greeter/.

There's a convenience officially designed here: A folder placed under ~/.claude/skills/ with a plugin.json manifest will automatically be loaded as my-greeter@skills-dir in the next session, no need to build a marketplace, no need to install. This is called a "skills directory plugin" and is the lightest path for developing self-use plugins.

Step 2: See what the scaffold generated

Just list the directory (note using absolute path):

bash
ls -R ~/.claude/skills/my-greeter

Expectation: You will see .claude-plugin/plugin.json and skills/ (or a SKILL.md). Confirm that plugin.json is inside .claude-plugin/, and the skill is outside—this is the physical manifestation of that iron rule from Section 01.

Step 3: Write a skill of your own

Open (or create) ~/.claude/skills/my-greeter/skills/hello/SKILL.md, and write the content as:

markdown
---
description: Say hello to the user with an enthusiastic tone
---

# Hello Skill

Enthusiastically greet the user named "$ARGUMENTS", asking how you can help today. Keep the tone friendly and encouraging.

$ARGUMENTS is a placeholder—it captures whatever text you type after the skill name. This is the standard way to pass parameters to a skill (Articles 26 and 27 covered skills, reusing it here).

Step 4: Load this plugin locally

During development, you don't need to build a marketplace, use --plugin-dir to directly load the local plugin directory, this is a flag officially dedicated to dev testing:

bash
claude --plugin-dir ~/.claude/skills/my-greeter

Expectation: Claude Code starts normally. Type /help, and you can see your skill listed under the plugin namespace.

Step 5: Call your skill and watch it run with your own eyes

Plugin skills always carry a namespace, so call it like this (followed by a name as a parameter):

text
/my-greeter:hello Walter

Expectation: Claude responds to you with an enthusiastic message carrying the name "Walter". Seeing it greet according to the description and body you wrote proves that this plugin was not only built correctly, but the skill it carries is truly usable.

Step 6: Change the skill, hot-reload to see the effect

Change a couple of sentences in the body of SKILL.md (e.g., make it use Chinese with emojis), then without restarting, type in Claude Code:

text
/reload-plugins

Expectation: Run /my-greeter:hello Walter again, and you can see the changes take effect.

⚠️ A difference explicitly stated officially: Changes to SKILL.md take effect immediately in the current session; but changes to hooks, .mcp.json, agents/, etc., require /reload-plugins or a restart to take effect. Don't think you wrote it wrong if you changed a hook and there's no reaction, reload it first.

Having run through these six steps, you have personally gone through the complete inner loop of plugin development: "build skeleton → write components → local load → invoke → hot-reload". Building any plugin in the future is essentially this workflow, just with more components.

💡 One-sentence summary: claude plugin init to start skeleton, --plugin-dir for local load, /reload-plugins for hot-reload—these three commands are the inner loop of plugin development; those placed in ~/.claude/skills/ can be automatically loaded without marketplace or installation.


06 Publishing the Plugin: Build a Marketplace

For self-use plugins, putting them in ~/.claude/skills/ or using --plugin-dir is enough. But to distribute it to your team or the community, you have to go through the "marketplace" layer—in Article 24 you were a "consumer" of the marketplace, in this section you act as the "shopkeeper".

First, clear up two concepts that look alike and are very easily confused, which the official docs specifically distinguished with a Note box:

ConceptWhat it isWhere defined
Marketplace sourceWhere to get that "product catalog" (marketplace.json)When user /plugin marketplace add, or in settings
Plugin sourceWhere to get the actual plugin for each catalog itemThe source field of each plugin entry in marketplace.json

Analogy: A shopping mall vs the supplier location for each item in the mall. "Marketplace source" is where this mall is opened and where the catalog brochure is taken from; "Plugin source" is which factory actually ships each item in the catalog. The two can completely be in different places—the mall catalog can hang in Warehouse A, while a certain item inside it ships directly from Factory B.

The core of a marketplace is one file: .claude-plugin/marketplace.json in the root directory of the repository. It declares the marketplace name, owner, and plugin manifest. A minimal one looks like this:

json
{
  "name": "my-plugins",
  "owner": { "name": "Your Name" },
  "plugins": [
    {
      "name": "my-greeter",
      "source": "./plugins/my-greeter",
      "description": "A friendly greeting plugin"
    }
  ]
}

Each plugin entry requires at least name and source (where to get this plugin). source supports several types of origins, this is the most practical part of the marketplace:

Plugin Source TypeHow to writeSuited for
Relative path"./plugins/my-greeter"Plugin is in the same repository as the marketplace (most common)
GitHub{ "source": "github", "repo": "owner/repo" }Plugin is in another GitHub repository
git-subdirGive url + pathPlugin is in a subdirectory of a large repo (monorepo), sparse checkout saves bandwidth
npm{ "source": "npm", "package": "@org/plugin" }Plugin published as an npm package

Hands-on: Try putting the plugin from Section 05 into a local marketplace. Assuming you built my-marketplace/ according to the structure above (inside it .claude-plugin/marketplace.json + plugins/my-greeter/), in Claude Code:

text
/plugin marketplace add ./my-marketplace
/plugin install my-greeter@my-plugins

Expectation: The first prompts marketplace added successfully; the second installs the plugin. Then /my-greeter:hello can be called—this chain is exactly the same as when you installed someone else's plugin in Article 24, except this time the goods are yours.

Always validate first before publishing, the official provides a dedicated command:

bash
claude plugin validate ./my-marketplace

Expectation: Checks the schema of marketplace.json, if there are duplicate plugin names, if source paths have illegal .., and if versions match up. When pointing to a marketplace directory, it only checks marketplace.json; to check the frontmatter of skills/agents along with it, you must point to the specific plugin directory (claude plugin validate ./my-marketplace/plugins/my-greeter).

To truly distribute to your team, push this marketplace repo to GitHub, and colleagues can add it with one command /plugin marketplace add owner/repo. To have the team install automatically, you can write extraKnownMarketplaces in the project's .claude/settings.json, and colleagues will be prompted to install when they trust the project directory—this belongs to team configuration, beginners just need to know this route exists.

💡 One-sentence summary: Publishing goes through a marketplace—create .claude-plugin/marketplace.json in root directory, list plugins and their source (relative path/github/npm etc.); distinguish between "marketplace source" (where catalog comes from) and "plugin source" (where each plugin comes from); claude plugin validate before publishing.


07 A Comparison Set: Self-use, Local Test, Official Release, Don't Take the Wrong Path of Three

The most common mistake people making plugins commit is not writing wrong code, but using the wrong "distribution method"—clearly just trying it out for yourself, but groaning to build a marketplace; or wanting to distribute to the team, but still using --plugin-dir which only works for yourself. This section nails down the three paths with one table.

Your SituationShould UseCommandCharacteristics
Purely self-use, want to change and use anytimeskills directory pluginPut in ~/.claude/skills/<name>/ (with plugin.json)Auto-loads, no marketplace/install needed, changes to SKILL.md effective immediately
In development, repeatedly testing a local package--plugin-dirclaude --plugin-dir ./my-pluginDirect load, not registered; can specify multiple times to load multiple; .zip works too
Distribute to team/communityMarketplacemarketplace.json + /plugin installCan version manage, auto-update, share

A few details you only know when you use them:

--plugin-dir is a development sharp tool, but it only lasts for the current session. You loaded it on startup this time, shut it down and open again and it's gone—it doesn't write into any config. The benefit is cleanliness: after testing the plugin, closing the session leaves it spotlessly clean, no trace left. A handy approach during development is keeping a session with --plugin-dir hanging constantly, modifying and /reload-plugins, much faster than reinstalling marketplace every time. There's also a thoughtful official design: A local copy from --plugin-dir will temporarily override an installed marketplace plugin of the same name—so you can take local changes and directly override-test an online plugin, without uninstalling first.

The skills directory plugin has a scope trap. Those placed in ~/.claude/skills/ (personal level) can be used by every project; but for those placed in the project's .claude/skills/ (project level), the official explicitly warns: It only loads from the directory where you start Claude Code, and will not search upwards to the repository root like regular skills do. So starting from a subdirectory will miss the plugins in the root directory—either start from the repo root, or /reload-plugins.

Don't build a marketplace right away. This is typical over-engineering. The official advice is very practical: First iterate quickly with loose files in .claude/ or --plugin-dir, then convert to a plugin and build a marketplace when ready to share. A plugin usually undergoes over ten rounds of changes under --plugin-dir before it stabilizes; building a marketplace during this period is purely making trouble for yourself—every change requires a commit and marketplace refresh.

💡 One-sentence summary: Don't take the wrong path among the three—stuff self-use in ~/.claude/skills/, use --plugin-dir for development, and only build a marketplace for publishing; --plugin-dir only lives one session (clean), iterate the plugin stably on the first two paths before publishing.


08 Two Unavoidable Pitfalls for Publishing: Version Management and Dependencies

A plugin being able to run and be installed doesn't mean it can be "published well". Version management and dependencies are the two places easiest to crash when publishing, both circled by official warning boxes, and this section is dedicated to filling them.

Pitfall 1: Set version, push new commit but nobody updates

This is the most counter-intuitive and deceiving one. How does Claude Code determine "if a plugin has a new version"? It pulls the version number in this order:

  1. version in plugin.json
  2. version in the marketplace entry
  3. If neither exists, uses the git commit SHA

The key is right here. The official warning is very stern:

Setting version pins the plugin. If plugin.json declares "version": "1.0.0", pushing new commits without changing that string does nothing for existing users, as Claude Code sees the same version and keeps its cached copy.

Translated into plain English: Once you hardcode "version": "1.0.0", just pushing new code to the repo is useless—the version number on the user's side hasn't changed, /plugin update will reply "already up to date", and the cache won't refresh at all. You must manually bump the version number up with every release (1.0.1, 1.1.0...).

So the official gives two strategies, choose one, don't mix:

StrategyHow to doUpdate behaviorSuited for
Explicit VersionSet version in plugin.json, bump manually on every releaseUsers only receive updates when you bump the version numberOfficial plugins with stable release rhythm
Commit SHA VersionDo not write version, host on gitEvery pushed new commit counts as a new version, users get it automaticallyInternal, team, rapidly iterating plugins

There's an even doubly deceiving one: don't write version in both plugin.json and the marketplace entry. The official explicitly states—the value in plugin.json will silently overwrite the one in the marketplace entry; you changing the version in marketplace.json might not take effect at all, because it's suppressed by that stale plugin.json version number.

The explicit version pitfall is easy to fall into: an internal team plugin set "version": "1.0.0", changed code several times and pushed it up, colleagues kept reporting "why hasn't it updated". You easily think the marketplace didn't refresh, and after tossing around for half a day you realize—the version number didn't move, so of course Claude Code thinks there's no change. Internal plugins uniformly do not write version and let it use SHA, push once and colleagues update once, and it's peaceful. Conclusion: rapidly iterating internal plugins do not set version, only stable release official plugins set it, and remember to bump every time.

Pitfall 2: How to Declare Dependencies

If your plugin A must have plugin B to work, declare it using the dependencies field:

json
{
  "name": "my-plugin",
  "dependencies": [
    "helper-lib",
    { "name": "secrets-vault", "version": "~2.1.0" }
  ]
}

After declaration, when installing/enabling your plugin, Claude Code will automatically install/enable the dependencies too. Versions can be constrained with semver (Semantic Versioning ranges) (like ~2.1.0), preventing a major version of a dependency from breaking your plugin. During uninstall, claude plugin uninstall --prune can conveniently clean up those plugins that "were auto-installed just to satisfy a dependency and are now unwanted by anyone"; plugins you manually installed directly will never be touched by prune.

Analogy: Marking "accessories required separately" on an ingredient list. A furniture manual writes at the end "This item requires purchasing M4 screw pack (2.1 spec) separately"—you prepare accessories according to the list, and only then can the furniture be assembled. dependencies is this accessory list for the plugin: it clearly writes "who do I need, and which version", and Claude Code automatically prepares them according to the list.

💡 One-sentence summary: Two publishing pitfalls—if version is set, you must manually bump it every release, otherwise users won't receive updates (just don't set it for rapid iteration, use SHA); dependencies are declared with dependencies and can add semver constraints, dependencies will be automatically installed along with your plugin.


09 Summary

This article advances plugins from "knowing how to use" to "knowing how to build and publish"—it is essentially a box of LEGOs: an instruction manual (plugin.json) determines identity, a few divided parts boxes (component folders) hold capabilities, and packing the whole box allows it to be sent to anyone.

String the core points together for review:

What you must figure outKey point
Directory SkeletonOnly plugin.json in .claude-plugin/, all other components placed in root directory
Manifest FieldsOnly required is name (sets namespace); component paths split into "replace" (agents/commands) and "append" (skills)
What Components can be packedskill/command/agent/hook/MCP/LSP/monitor seven types; agents in plugins are stripped of power (no hook/MCP/permission mode)
How to write pathsMust use variables like ${CLAUDE_PLUGIN_ROOT}, no hardcoding, no referencing files outside directory; state goes in DATA not ROOT
Dev Inner Loopplugin init starts skeleton → --plugin-dir loads → /reload-plugins hot reloads
PublishingBuild marketplace.json, distinguish marketplace source/plugin source, claude plugin validate to verify
Two PitfallsIf version is set it must be bumped every time; dependencies declared with dependencies

You should now be able to: Understand what the directory of any plugin is doing, build a runnable plugin skeleton from scratch yourself, test locally with --plugin-dir, and then build a marketplace to distribute it to your team; and also clearly know why ${CLAUDE_PLUGIN_ROOT} cannot be hardcoded, and how to avoid the version pitfall of "no update if not bumped". In Article 24 you learned to consume other people's plugins, in this article you became someone who can produce plugins—packing that set of configurations you accumulated for a long time into a package and publishing it, no longer just copying others'.

Up to here, the "extension + configuration + packaging & distribution" line of Claude Code is complete. From CLAUDE.md in Article 18, MCP in Article 22, subagent in Article 23, to the plugins in Articles 24 and 38—you not only know how to use these tools, but also how to assemble, package, and deliver them to others.


Next article 39 "Getting Started Practice"—the previous thirty-some articles were all "explaining features one by one dismantled", the parts in your hands are already piled like a mountain. The next article will no longer teach new features, but will string these parts into a complete practical path for the first time: taking a real small requirement, walking through from start to delivery, letting you see with your own eyes "how the things learned twist into a rope to work". Think about it—every trick you know now is handy to use alone, but when fighting a real battle, which one should be pulled first, and which one to follow?