Host a marketplace
Put a plugin marketplace where people can reach it, give them access to private repos, ship updates, and rename plugins without breaking installs.
Once your marketplace.json works locally, hosting is about three things: putting the catalogue somewhere your users can fetch it, making sure their machines can authenticate, and releasing changes in a way that actually reaches them. This page is written for whoever runs the marketplace.
If you have not written the catalogue yet, start with Create a marketplace. If you are an administrator forcing marketplaces onto company machines, Manage plugins for your organisation is the better starting point.
Tip: Read the section on versions before your first release, and the section on renames before you ever change a plugin's
name. Those are the two places where marketplace owners most often strand their users.
Choosing where to host
You have four options. Whichever you pick, send your users the matching add command and tell them what their machine needs.
| Host | Command users run in a session | Prerequisites on their machine |
|---|---|---|
| GitHub | /plugin marketplace add acme/claude-plugins | git; for a private repo, working credentials (see below) |
| Any other git host (GitLab, Bitbucket, GitHub Enterprise Server) | /plugin marketplace add https://git.acme.internal/platform/claude-plugins.git | git and network access to the host. Always use the full URL, because owner/repo shorthand is assumed to mean github.com |
A marketplace.json served over HTTPS | /plugin marketplace add https://plugins.acme.dev/marketplace.json | HTTPS access to that URL. No git needed for the catalogue itself |
| A folder on a shared drive | /plugin marketplace add /Volumes/eng-share/claude-plugins | Read access to the path |
For GitHub or git URLs, users can pin a branch or tag by appending #<ref>, for example acme/claude-plugins#stable. All accepted forms are listed under plugin marketplace add in the plugin CLI reference.
A successful add reports Successfully added marketplace: <name>. The name comes from the name field inside marketplace.json, not from the repository name, and it is what users type after the @ when installing, for example /plugin install lint-rules@claude-plugins.
Registering for a whole repository
If everyone who works in a particular repository should have your marketplace, run this once from that repository's root and commit the result:
claude plugin marketplace add acme/claude-plugins --scope project
It writes the registration into .claude/settings.json. Each teammate gets the marketplace once they trust the folder. See Manage plugins for your organisation for how per-repository requirements behave.
Hosting a bare catalogue URL
When users add a plain marketplace.json URL, Claude Code downloads that one file and nothing else. Any entry whose source is a relative path then has nothing to point at and fails on install with its marketplace entry path does not stay inside the marketplace directory. For a URL-hosted catalogue, give every entry a source that can be fetched by itself, such as github or archive. Alternatively host in git so the whole tree is cloned.
Download limits
Files fetched over HTTP are subject to hard limits. Size your files and configure your server accordingly.
| What is downloaded | Maximum size | Server response deadline | Redirect rules |
|---|---|---|---|
marketplace.json from a url marketplace | 5 MiB | 10 seconds | A cross-origin redirect must stay on https:// and must not target loopback, link-local or cloud metadata hosts. Downgrading to http:// fails |
Zip from an archive plugin source | 256 MiB | 120 seconds | No more than five redirects; every hop must be https:// and must avoid loopback, link-local and metadata hosts |
Headers you configured are not forwarded when a redirect crosses to a different origin.
Once downloaded, an archive is also rejected at extraction if it exceeds any of these:
- 100,000 files and directories in total.
- 512 MiB uncompressed for any single file.
- 1 GiB uncompressed in total.
- An uncompressed size more than 50 times the zip size.
Shared folders load live
For a marketplace added from a shared directory, relative-path plugins are read in place rather than copied. Your edits appear at the user's next session start or after /reload-plugins, with no version bump.
Avoid Git LFS
Claude Code clones git-hosted marketplaces and plugins without fetching LFS content, so any LFS-tracked file arrives as a pointer stub. Keep everything a plugin needs in ordinary git objects.
Sharing files with symlinks
You can symlink files between plugins in the same marketplace. When a plugin is copied into the cache, each link is treated according to where it resolves:
- Inside the plugin's own folder: kept as a relative symlink in the cache.
- Elsewhere in the same marketplace: dereferenced, with the target's content copied in. This is how a "bundle" plugin can expose skills that live in sibling plugins.
- Outside the marketplace: dropped, for safety.
Plugins installed from a local path, or from a command source in its default copy mode, are stricter: only links that stay inside the plugin's own folder survive.
# from inside plugins/frontend-bundle
ln -s ../../react-review/skills/component-audit ./skills/component-audit
On Windows, create directory links with mklink /D from an elevated prompt or with Developer Mode enabled.
Distributing through claude.ai organisation settings
On Team and Enterprise plans there is another route: an admin connects the repository under Organization settings > Plugins & skills on claude.ai, and it syncs through the organisation's GitHub or GitLab connection. Users' own git credentials never come into it.
Organisation sync is pickier than /plugin marketplace add:
- On github.com and gitlab.com the repository must be private or internal.
- Only a subset of plugin source types is accepted.
- A plugin with a top-level
bin/directory is rejected (the error beginsPlugin contains a top-level bin/ directory), though the rest of the marketplace still syncs. Move executables to something likescripts/and reference them as${CLAUDE_PLUGIN_ROOT}/scripts/<name>from hooks or MCP configs.
Private repositories and credentials
When a user adds, installs from or updates your marketplace, Claude Code shells out to git with interactive prompts disabled. It has no token of its own and marketplace.json has no field for one, so everything depends on credentials already present on the user's machine.
The form of the add command decides the protocol:
owner/repoon GitHub: Claude Code triesssh -T git@github.com. If that works it clones over SSH; if the probe or the SSH clone fails, it falls back to HTTPS. Users without a GitHub SSH key can setCLAUDE_CODE_PLUGIN_PREFER_HTTPS=1to skip straight to HTTPS.git@host:path.git: SSH.https://…: HTTPS.
What each protocol needs:
- SSH: a key usable without a passphrase prompt (typically loaded in
ssh-agent), and the host already inknown_hosts. - HTTPS: the user's git credential helper stays enabled but is not allowed to prompt. A stored credential works; one that would need asking for fails. On GitHub,
gh auth loginthengh auth setup-gitstores one.
GitHub Enterprise Server users need git access to that host; GitHub Enterprise Server covers what each Claude Code surface requires.
Users without a git account
People with no account on your git host can still add a catalogue served as a URL or from a shared folder, but they can only install entries whose sources they can reach. A private github entry still fails for them. Sources that need no account:
archive: a zip over HTTPS. Nogitand no account, just network access. Needs Claude Code v2.1.224 or later. Pin each one withsha256so a tampered download is refused, and see authenticating archive downloads if the server needs credentials.- A public git repository: a
urlorgit-subdirsource with anhttps://URL clones anonymously. Forgithubsources (orgit-subdirwritten asowner/repo), users without an SSH key should setCLAUDE_CODE_PLUGIN_PREFER_HTTPS=1.
On a single office network, a shared-folder marketplace also avoids git accounts entirely.
Background updates and credentials
Background auto-update (off by default, see below) checks for new commits after a session starts. For a private marketplace it uses the user's credential helpers without prompting:
| Remote and helper | Result |
|---|---|
SSH with a key in ssh-agent | Authenticates |
HTTPS with a helper holding a stored credential (Git Credential Manager, macOS Keychain, git-credential-store) | Authenticates |
| HTTPS with a helper that would need to prompt | Fails silently; the existing checkout stays and plugins keep working |
If the checkout is current, nothing happens. If there are new commits, or the remote cannot be reached or authenticated, Claude Code re-clones and swaps in the fresh copy; if the clone fails, the old checkout remains. On big repositories that re-clone can hit the 120 second clone timeout described in plugin troubleshooting.
Users can keep a private marketplace healthy by storing a credential first (gh auth login, gh auth setup-git), or by setting CLAUDE_CODE_PLUGIN_KEEP_MARKETPLACE_ON_FAILURE=1 so an unreachable remote leaves the existing checkout alone instead of triggering a re-clone.
Note that exporting GITHUB_TOKEN or a similar variable does nothing by itself. A token only counts when a credential helper (such as the one gh installs, which reads GH_TOKEN and GITHUB_TOKEN) hands it to git.
Rolling out to a company
A company-wide rollout involves three parties, though you can do without the administrator if people are happy to install things themselves.
| Role | Responsibility |
|---|---|
| Marketplace owner (you) | Host the catalogue somewhere only the company can read, publish the add command, and document what each machine needs |
| Administrator | Register the marketplace and enable its plugins fleet-wide via extraKnownMarketplaces and enabledPlugins in managed settings, and set autoUpdate there. See Manage plugins for your organisation |
| Each user | Have read access and stored credentials for the repo. Without an admin, also run the add and install commands (Install plugins) |
For people with no git account, combine the options above: account-free entry sources, a pre-seeded plugins directory (covered under seeding containers and CI on the org page), or claude.ai organisation sync.
Getting updates to users
Users receive changes either through background auto-update or by updating manually. Either way, they only get a new copy when the plugin's computed version changes.
Turning on auto-update
Auto-update is off for third-party marketplaces and there is no marketplace.json field to change that. Either:
- each user opens
/plugin, goes to Marketplaces, picks yours and chooses Enable auto-update; or - an admin sets
"autoUpdate": trueon your marketplace'sextraKnownMarketplacesentry in managed settings.
Without it, users pull changes with /plugin marketplace update <marketplace> in a session or claude plugin update <plugin>@<marketplace> from the shell. How plugins load describes what happens when an update lands.
Versioning releases
The version Claude Code compares is taken from plugin.json first, then from the marketplace entry. You have two sensible strategies:
- Bump
versionevery release. Users sit on their cached copy until the string changes. Pushing commits without bumping means nobody receives them. This catches people out more than anything else. - Leave
versionout entirely, from bothplugin.jsonand the entry. Users then track your commits.
Never set it in both places. Claude Code silently prefers plugin.json, and claude plugin validate flags the disagreement as Entry declares version "<a>" but <path>/plugin.json says "<b>".
Two exceptions ignore version altogether: plugins loaded in place from a locally added marketplace (they always read current files), and command sources.
Holding users on a version
A marketplace serves exactly one version of each plugin, so pinning happens through what the entry points at:
ref(branch or tag) andsha(commit) ongithub,urlandgit-subdirsources.#<ref>on the add command, which pins the whole catalogue.<plugin>--v<version>git tags, which dependency ranges resolve against (see Plugin dependencies).
Changing a command source
If you alter the command of a command source, or switch its mode, every user must re-approve it, because Claude Code only ever runs the exact command a user accepted. Until they do, the once-per-session background run stops for them and an entry appears in the /plugin Errors tab showing the new command and the claude plugin update invocation to run. Tell users to run that in a terminal; they will be shown the new command and asked to accept.
Release channels
Claude Code has no built-in notion of channels. To offer stable and preview tracks, host two catalogues with different name values (two marketplaces with the same name cannot be registered together) and point their entries at different refs:
{
"name": "acme-plugins-stable",
"owner": { "name": "Acme Platform Team" },
"plugins": [
{
"name": "deploy-guard",
"source": { "source": "github", "repo": "acme/deploy-guard", "ref": "release" }
}
]
}
The preview catalogue is identical apart from "name": "acme-plugins-preview" and "ref": "main". Make sure the two refs carry different plugin.json versions, or omit version so the commit SHA distinguishes them; a ref that moves without a version change leaves users stuck on their cache. Admins can assign channels to groups by handing each group a different extraKnownMarketplaces entry.
Renaming and removing plugins
A plugin's name is its identity. It is stored in users' enabledPlugins and pluginConfigs settings and typed into install commands, so changing it breaks existing installs. If you only want a friendlier label in /plugin, set displayName in plugin.json and leave name alone.
The renames map
When a rename or removal is unavoidable, add a top-level renames object to marketplace.json. Map each old name to its new one, or to null if the plugin is gone:
{
"name": "acme-plugins",
"owner": { "name": "Acme Platform Team" },
"plugins": [
{ "name": "deploy-guard", "source": "./plugins/deploy-guard" }
],
"renames": {
"release-checks": "deploy-guard",
"old-changelog-bot": null
}
}
After you push, users with the old name enabled get:
- For a rename: the plugin loads under its new name, a one-off notice
Renamed to "deploy-guard" in the "acme-plugins" marketplaceappears inclaude plugin listand in/plugin, and the key is rewritten inenabledPluginsandpluginConfigsacross user, project and local settings. - For
null: the key is removed from those scopes and they seeRemoved from the "acme-plugins" marketplace. - If the plugin was enabled by managed settings: it loads under the new name, but managed settings cannot be rewritten, so the notice repeats until an admin updates them.
For git or URL marketplaces, a renamed plugin reports Plugin "<name>" not cached at <path> until the user runs /plugin install <new-name>@<marketplace> once.
Treat the map as an append-only log. Keep old entries forever, and on a second rename add a new entry rather than editing the existing one, since Claude Code follows the chain from the oldest name. claude plugin validate . rejects cycles or chains that end anywhere other than null or a listed plugin, with renames.<name>: chain does not resolve.
Cleaning up removed plugins
By default, a plugin you delete from the catalogue stays installed and complains with Plugin "<name>" not found in marketplace. Set "forceRemoveDeletedPlugins": true at the top level and, at every session start, Claude Code will:
- Compare the user's installs from your marketplace with the current entries and
renames, treating anything neither listed nor renamed as removed. - Uninstall those plugins from user, project and local scopes, leaving managed-settings installs alone.
- Show them under a Flagged heading in
/pluginwith statusRemoved from marketplace.
Authenticating archive downloads
For archive sources on a private registry, you can attach HTTP headers in two places:
- On the marketplace's
urlsource, wherever that is declared (for example anextraKnownMarketplacesentry in settings). - On the plugin entry itself, next to
source. This needs v2.1.238 or later.
In either place you can use headersHelper instead of a static headers object: a command that prints the headers as JSON, which suits short-lived tokens. That also needs v2.1.238 or later.
| Where it is set | Which downloads receive the headers | When a headersHelper runs |
|---|---|---|
Marketplace url source | Archives on the same origin (scheme, host and port) as the marketplace URL | Before each fetch of marketplace.json and before each same-origin archive download; output is reused for up to 60 seconds |
| Plugin entry | That one entry's download | Only when the user installs or updates that single plugin and accepts the command |
If both places set the same header, the entry wins. Within one place, a header printed by the helper overrides the same name in headers.
Example entry
An entry using headersHelper must also set "strict": false:
{
"name": "billing-schemas",
"description": "Skills for working with the billing service's protobuf schemas",
"strict": false,
"source": {
"source": "archive",
"url": "https://artifacts.acme.internal/claude/billing-schemas-3.4.0.zip"
},
"headersHelper": "/usr/local/bin/artifactory-token --format claude-headers"
}
Test it with claude plugin install billing-schemas@acme-plugins. You will be shown the command and URL and asked to accept before anything runs.
Rules for the helper command
- At most 500 printable ASCII characters, with no run of four or more spaces.
- Must print a single JSON object of string header values on stdout and exit 0 within 10 seconds, for example
{"Authorization": "Bearer abc123"}. - Runs via
sh(orcmd.exeon Windows) with the config directory (~/.claude, orCLAUDE_CONFIG_DIR) as the working directory, so use absolute paths or commands onPATH. - When set in a
marketplace.jsonentry or a project's.claude/settings.jsonor.claude/settings.local.json, any environment variable that looks like a credential is stripped first, using the same rule as MCP header helpers (see MCP). BothANTHROPIC_API_KEYand something likeARTIFACTORY_TOKENdisappear, so read secrets from a file or keychain. User settings,--settingsfiles and managed settings are exempt. - Claude Code provides
CLAUDE_CODE_MARKETPLACE_URLandCLAUDE_CODE_MARKETPLACE_NAMEto aurlsource's helper, andCLAUDE_CODE_PLUGIN_NAMEandCLAUDE_CODE_PLUGIN_ARCHIVE_URLto an entry's helper. The marketplace name is unset on the very first fetch after adding by URL, since that fetch is what discovers it.
When headers are skipped or dropped
- The helper fails (non-zero exit, more than 10 seconds, or output that is not a JSON object of strings): the fetch or download it was for does not happen.
- The marketplace URL is not
https://: its helper never runs; only staticheadersare sent. - A redirect leaves the origin: neither static nor helper headers from either place go with it.
- Routing or identity headers in an entry: names such as
Host,CookieandX-Forwarded-*are stripped from everymarketplace.jsonentry's headers and helper output; authentication headers likeAuthorizationsurvive. - Helper declared in an
--add-dirdirectory's settings: ignored; only that file's staticheadersapply. - Managed policy:
disableCommandPluginSources: trueblocks helpers, and so doesallowManagedHooksOnlyunlessdisableCommandPluginSourcesis explicitlyfalse. Helpers on marketplaces declared by managed settings themselves still run.
Accepting an entry's helper
Users accept an entry's helper every time they install or update that plugin on its own, via its view in /plugin, claude plugin install or claude plugin update. In scripts, --yes accepts it, or --accept-command <sha256> accepts only the command a previous --json run reported. If the command or archive URL changed in the meantime the operation is refused (a changed query string alone does not count).
Any other kind of operation never runs an entry's helper or downloads its archive:
- Bulk installs, installs from a plugin suggestion, or installs as a dependency: that plugin is refused and the user is pointed to its own
/pluginview. Other plugins in a bulk install proceed, but anything depending on the refused plugin waits until it is installed by hand. - Background auto-update, or session start for a never-downloaded archive: the plugin is listed in the
/pluginErrors tab.
When a marketplace-level helper runs
A url source's helper lives in a settings file, not in your catalogue, so users are not asked on every install. The file it is declared in decides when it runs:
| Declared in | Runs |
|---|---|
User settings, a --settings file, or local managed settings | Without asking, including during background refreshes |
A project's .claude/settings.json or .claude/settings.local.json | Only after the user accepts the workspace trust dialog for that exact folder. -p and SDK sessions do not count, and nor does trust on a parent folder |
| Server-managed settings | In interactive sessions, only after the user approves the delivered settings in the security approval dialog (see Server-managed settings) |
Inline plugin entries in those files need the same trust or approval, plus per-install acceptance of the entry's own helper.
Dependencies and suggestions
Entries can depend on other plugins, optionally with a semver range. Dependencies from a different marketplace only install when your catalogue lists that marketplace in allowCrossMarketplaceDependenciesOn. Plugin dependencies covers ranges and tagging.
To have Claude Code recommend one of your plugins when a project looks relevant, add a relevance block to the entry. Suggestions only appear for marketplaces an admin lists in pluginSuggestionMarketplaces; see Suggest plugins by relevance.
Things a marketplace cannot do
Owners regularly ask for features that do not exist as catalogue fields. The closest equivalents:
| You want to | Use instead |
|---|---|
| Stop users installing from other marketplaces | The managed setting strictKnownMarketplaces (org guide) |
| Install or enable a plugin without the user asking | Managed enabledPlugins |
| Show different plugins to different people | Separate marketplaces; every user sees the whole catalogue |
| Mark a plugin deprecated | Remove the entry, map it to null in renames, optionally set forceRemoveDeletedPlugins |
| Switch on auto-update for everyone | Users toggle it in /plugin, or an admin sets autoUpdate in managed settings |
| Ship a git token | Not possible; rely on users' git setup, or use headers / headersHelper on archive sources |