Claude Code Plugin Marketplace: Authoring marketplace.json

A marketplace is a catalog that distributes Claude Code plugins. It is a single file, .claude-plugin/marketplace.json, at the root of a repository, listing a name, an owner and an array of plugins with their sources. Users add the marketplace once, then install individual plugins from it by name.
Almost always the version field. If you set version on a plugin entry, the plugin is pinned to that string and users receive an update only when the string changes, so pushing a fix without bumping it leaves everyone on their cached copy. If you omit version on a git source, the version comes from the resolved commit and every push is an update.
A relative path is the simplest when plugins live in the same repository as the marketplace. Use a github or url source when a plugin has its own repository and release cycle, git-subdir for a directory inside a monorepo, and npm if you already publish there. Archive and command sources exist for build systems rather than for people.
Add a renames object to marketplace.json mapping the old name to the new one, and Claude Code follows the rename chain automatically for users who installed the old name. Mapping an old name to null marks the plugin as removed. This requires Claude Code v2.1.193 or later.
Add extraKnownMarketplaces to the project's .claude/settings.json so the marketplace arrives with the repository once the folder is trusted, and list the plugins under enabledPlugins. Organisations can also restrict which marketplaces are permitted at all with the strictKnownMarketplaces managed setting.

Key Takeaway
A Claude Code plugin marketplace is one file, .claude-plugin/marketplace.json, at the root of a repository. It needs a name, an owner and a list of plugins, each with a source. The single decision that causes the most support work is the version field: set it and bump it on every release, or your users quietly keep running a cached copy.
I published an internal marketplace, pushed a fix to a plugin, told the team to update, and watched three people report the old behaviour anyway. Nothing was broken. I had simply left version out of one entry and set it in another, and the two behave in opposite ways — which is the kind of detail you find out from your colleagues rather than from a stack trace.
This post is the authoring side of plugins: the manifest and every field that matters, the seven source types and when each is the right answer, how versioning actually decides who gets your update, renames and strict mode, validating before you publish, getting the marketplace onto a team's machines without asking anyone to type a command, and locking down which marketplaces an organisation permits at all.
Only name, owner and plugins are required, and each plugin entry needs only name and source. Everything else — displayName, description, version, author, license, keywords, category — exists so the entry reads well in the Discover tab and so you can control updates. Here is a realistic file rather than a minimal one:
// .claude-plugin/marketplace.json — at the repository root
{
"name": "acme-tools",
"owner": { "name": "Acme Platform", "email": "[email protected]" },
"description": "Internal tooling for Acme engineers",
"metadata": { "pluginRoot": "./plugins" },
"plugins": [
{
"name": "code-formatter",
"source": "formatter", // bare name, needs pluginRoot
"displayName": "Code Formatter",
"version": "2.1.0", // bump this on EVERY release
"author": { "name": "Acme DevTools" },
"license": "MIT",
"category": "productivity"
},
{
"name": "deploy-kit",
"source": {
"source": "github",
"repo": "acme-corp/deploy-plugin",
"ref": "v2.0.0",
"sha": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"
}
}
],
"renames": { "formatter": "code-formatter", "legacy-linter": null }
}Two details are easy to miss. Relative paths resolve from the marketplace root, not from the .claude-plugin directory that holds the file. And a set of marketplace names is reserved for Anthropic — claude-plugins-official, anthropic-plugins, agent-skills and a dozen more — along with names that impersonate them, so pick something that is obviously yours.
The source field decides where the plugin is fetched from, and the choice has more consequences than it looks. A relative path keeps everything in one repository and is the simplest thing that works. A github or url source lets a plugin live in its own repository with its own release cycle. The rest exist for specific situations.
Choosing a source type:
| Source | When it is the right answer |
|---|---|
| Relative path | Plugins live in the same repository as the marketplace. Must start with a dot-slash, unless you set metadata.pluginRoot and use a bare directory name |
| github or url | The plugin has its own repository. Optional ref pins a branch or tag; optional sha pins an exact 40-character commit |
| git-subdir | The plugin is a directory inside a large monorepo. Sparse clone, so you do not pull the whole thing |
| npm | You already publish to a registry. Takes package, an optional version range, and a custom registry URL |
| archive or command | A zip over HTTPS with an optional sha256 for integrity, or a local command that prints a plugin directory. Both are for build systems, not for people |
Two constraints bite in practice. A marketplace distributed as a bare marketplace.json URL cannot resolve relative paths at all, so a file that works from a git clone breaks when you host it as a plain file — use another source type. And archives must be HTTPS end to end, including every redirect, with a 256 MiB ceiling.
This is the field worth understanding properly, because both of its failure modes look like something else. What version does depends on whether you set it and on what kind of source you used:
Two release channels cost nothing to run: publish the same plugin twice, in a stable marketplace pinned to a stable branch and a latest marketplace pinned to latest. People who want the new thing add the second marketplace; everyone else never notices you shipped. That is far less work than trying to encode risk tolerance in a version range.
Renaming a plugin normally strands everyone who installed the old name. The renames object fixes that: map the old name to the new one and Claude Code follows the chain automatically, or map it to null to mark a plugin removed. It needs v2.1.193 or later, and it costs one line, so add it the first time you rename anything rather than promising yourself you will remember.
Strict mode decides who owns the component list. Left at its default of true, the plugin's own plugin.json is the authority and your marketplace entry can add extra components on top. Set it to false and the marketplace entry becomes the entire definition, which conflicts if plugin.json also declares components. Use false only when you are the operator deliberately controlling what a third-party plugin exposes.
There is a real validator, and it catches the errors that are otherwise invisible until a user reports them: syntax, duplicate plugin names, source paths that traverse out of the repository, invalid local plugin.json files, and version mismatches between the entry and the plugin. Unrecognised fields come back as warnings rather than errors, and a near-miss field name gets a suggestion — pass strict when you want warnings to fail the run.
# Add a marketplace — GitHub shorthand, a git URL, or a path
claude plugin marketplace add acme-corp/claude-plugins
claude plugin marketplace add acme-corp/[email protected]
claude plugin marketplace add https://gitlab.com/team/plugins.git
claude plugin marketplace add ./my-marketplace --scope project
claude plugin marketplace list [--json]
claude plugin marketplace update [name]
claude plugin marketplace remove <name> [--scope <scope>]
# Validate BEFORE you publish. This is the whole quality gate.
claude plugin validate .
claude plugin validate ./my-plugin --strict # warnings become errors
# What a user actually runs
/plugin install code-formatter@acme-tools
/plugin install code-formatter@acme-tools --scope projectThe path that scales is settings rather than instructions. Put extraKnownMarketplaces in the project's settings file and the marketplace arrives with the repository once the folder is trusted; add enabledPlugins and the plugins arrive with it. Note one v2.1.195 change worth planning around: adding a marketplace this way no longer installs plugins that come from an external source, so a member sees the install command instead of a working plugin.
// .claude/settings.json — the marketplace arrives with the repo
{
"extraKnownMarketplaces": {
"acme-tools": {
"source": { "source": "github", "repo": "acme-corp/claude-plugins" },
"autoUpdate": true // managed settings only
}
},
"enabledPlugins": { "code-formatter@acme-tools": true }
}
// Managed settings — an allowlist of what may be added at all.
{
"strictKnownMarketplaces": [
{ "source": "github", "repo": "acme-corp/*" },
{ "source": "hostPattern", "hostPattern": "^github\\.example\\.com$" }
]
}
// undefined = no restriction. [] = total lockdown, official
// marketplace included.For a managed environment, strictKnownMarketplaces is an allowlist of the marketplaces users are permitted to add. Leave it undefined and there is no restriction; set it to an empty array and you have total lockdown, official marketplace included. Entries match on a GitHub repository, including an owner wildcard, on an exact URL, or on a regular expression against the host or filesystem path.
Two smaller levers are worth knowing. A marketplace entry can carry a relevance object, and when an administrator allowlists the marketplace through the pluginSuggestionMarketplaces managed setting, matching plugins are pinned at the top of the Discover tab as suggested for this directory. And defaultEnabled set to false lets you ship a plugin that installs without switching itself on, which is the polite default for anything that costs money or calls an external service.
Start with one repository, relative-path sources and an explicit version on every entry, because that combination has the fewest surprises. Run the validator in whatever checks the repository already has, so a broken manifest never reaches a colleague. And write the renames entry the first time you rename a plugin, not the second time, when someone tells you their install disappeared.
Sources & further reading