Skip to content

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.

HostCommand users run in a sessionPrerequisites on their machine
GitHub/plugin marketplace add acme/claude-pluginsgit; 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.gitgit 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.jsonHTTPS access to that URL. No git needed for the catalogue itself
A folder on a shared drive/plugin marketplace add /Volumes/eng-share/claude-pluginsRead 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 downloadedMaximum sizeServer response deadlineRedirect rules
marketplace.json from a url marketplace5 MiB10 secondsA 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 source256 MiB120 secondsNo 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.

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 begins Plugin contains a top-level bin/ directory), though the rest of the marketplace still syncs. Move executables to something like scripts/ 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/repo on GitHub: Claude Code tries ssh -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 set CLAUDE_CODE_PLUGIN_PREFER_HTTPS=1 to 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 in known_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 login then gh auth setup-git stores 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. No git and no account, just network access. Needs Claude Code v2.1.224 or later. Pin each one with sha256 so a tampered download is refused, and see authenticating archive downloads if the server needs credentials.
  • A public git repository: a url or git-subdir source with an https:// URL clones anonymously. For github sources (or git-subdir written as owner/repo), users without an SSH key should set CLAUDE_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 helperResult
SSH with a key in ssh-agentAuthenticates
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 promptFails 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.

RoleResponsibility
Marketplace owner (you)Host the catalogue somewhere only the company can read, publish the add command, and document what each machine needs
AdministratorRegister 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 userHave 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": true on your marketplace's extraKnownMarketplaces entry 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:

  1. Bump version every 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.
  2. Leave version out entirely, from both plugin.json and 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) and sha (commit) on github, url and git-subdir sources.
  • #<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" marketplace appears in claude plugin list and in /plugin, and the key is rewritten in enabledPlugins and pluginConfigs across user, project and local settings.
  • For null: the key is removed from those scopes and they see Removed 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:

  1. Compare the user's installs from your marketplace with the current entries and renames, treating anything neither listed nor renamed as removed.
  2. Uninstall those plugins from user, project and local scopes, leaving managed-settings installs alone.
  3. Show them under a Flagged heading in /plugin with status Removed from marketplace.

Authenticating archive downloads

For archive sources on a private registry, you can attach HTTP headers in two places:

  • On the marketplace's url source, wherever that is declared (for example an extraKnownMarketplaces entry 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 setWhich downloads receive the headersWhen a headersHelper runs
Marketplace url sourceArchives on the same origin (scheme, host and port) as the marketplace URLBefore each fetch of marketplace.json and before each same-origin archive download; output is reused for up to 60 seconds
Plugin entryThat one entry's downloadOnly 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 (or cmd.exe on Windows) with the config directory (~/.claude, or CLAUDE_CONFIG_DIR) as the working directory, so use absolute paths or commands on PATH.
  • When set in a marketplace.json entry or a project's .claude/settings.json or .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). Both ANTHROPIC_API_KEY and something like ARTIFACTORY_TOKEN disappear, so read secrets from a file or keychain. User settings, --settings files and managed settings are exempt.
  • Claude Code provides CLAUDE_CODE_MARKETPLACE_URL and CLAUDE_CODE_MARKETPLACE_NAME to a url source's helper, and CLAUDE_CODE_PLUGIN_NAME and CLAUDE_CODE_PLUGIN_ARCHIVE_URL to 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 static headers are 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, Cookie and X-Forwarded-* are stripped from every marketplace.json entry's headers and helper output; authentication headers like Authorization survive.
  • Helper declared in an --add-dir directory's settings: ignored; only that file's static headers apply.
  • Managed policy: disableCommandPluginSources: true blocks helpers, and so does allowManagedHooksOnly unless disableCommandPluginSources is explicitly false. 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 /plugin view. 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 /plugin Errors 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 inRuns
User settings, a --settings file, or local managed settingsWithout asking, including during background refreshes
A project's .claude/settings.json or .claude/settings.local.jsonOnly 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 settingsIn 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 toUse instead
Stop users installing from other marketplacesThe managed setting strictKnownMarketplaces (org guide)
Install or enable a plugin without the user askingManaged enabledPlugins
Show different plugins to different peopleSeparate marketplaces; every user sees the whole catalogue
Mark a plugin deprecatedRemove the entry, map it to null in renames, optionally set forceRemoveDeletedPlugins
Switch on auto-update for everyoneUsers toggle it in /plugin, or an admin sets autoUpdate in managed settings
Ship a git tokenNot possible; rely on users' git setup, or use headers / headersHelper on archive sources