Publishing a Private Plugin Marketplace for Your Team
A private plugin marketplace is a git repository with a catalogue file (.claude-plugin/marketplace.json) that lists your team’s plugins. Claude Code installs from it, Codex 0.157.1 reads the same file, and Cursor imports its own catalogue as a team marketplace. A change reaches teammates only when the plugin’s version changes, so CI must enforce the bump.
You packaged the team’s release checklist and API conventions as plugins, pushed them, and told everyone to install. Two weeks later you fix a wrong rule in the API skill, and half the team still runs the old one. Nobody bumped the version, one person’s clone of the private repository fails silently, and a new hire has enabledPlugins committed but no plugin installed. This page is for the tech lead who owns the marketplace and the developer who ships changes to it. It assumes you can already build a plugin; here you run the channel that delivers it.
What a working team marketplace gives you
Section titled “What a working team marketplace gives you”- A two-plugin marketplace repository,
acme-plugins, with every file shown and tested on Claude Code 2.1.283 and codex-cli 0.157.1 - A CI gate,
scripts/check-marketplace.sh, that fails a pull request on a schema warning, a reserved name, a missing source directory or a change without a version bump - The release loop: pull request, CI, merge, tag, and one update command per teammate
- Four rollout routes (per person, per repository, fleet-wide, claude.ai organization sync) with what each one does and does not install
- A recovery list for the failures that leave teammates on stale plugins
Everything marked as observed below was run on 2026-09-26 in a scratch home directory. The Cursor catalogue was checked against the schemas in Cursor’s cursor/plugins repository; the dashboard import comes from secondary sources, because cursor.com was unreachable, and was not run.
How a change travels from a pull request to every teammate
Section titled “How a change travels from a pull request to every teammate”The marketplace has one job: make sure that what is merged is what every teammate runs. Each stage has a check that replaces reading the change line by line.
| Stage | Who | Check that proves it | Tool |
|---|---|---|---|
| Pull request | Plugin author | CI: strict validation, install smoke test, version-bump rule | scripts/check-marketplace.sh |
| Review | Owner in CODEOWNERS | CI green, version bumped, always-on token cost read from CI log | GitHub review |
| Release | Marketplace owner | claude plugin tag confirms plugin.json and the catalogue agree | claude plugin tag --push |
| Rollout | Every teammate, or managed settings | claude plugin list shows the new version | claude plugin marketplace update, claude plugin update |
Build the two-plugin marketplace repository
Section titled “Build the two-plugin marketplace repository”The example marketplace ships two plugins. acme-release holds a release-check skill; acme-api-style holds the team’s HTTP API conventions. Both are skills only, so they cost little context and carry across agents.
-
Lay out the repository. Keep each plugin under
plugins/and the catalogue at the root.acme-plugins/├── .claude-plugin/marketplace.json # the catalogue; Codex reads it too├── .cursor-plugin/marketplace.json # optional: Cursor's own catalogue├── .github/CODEOWNERS├── .github/workflows/plugin-marketplace.yml├── scripts/check-marketplace.sh└── plugins/├── acme-release/│ ├── .claude-plugin/plugin.json│ └── skills/release-check/SKILL.md└── acme-api-style/├── .claude-plugin/plugin.json└── skills/api-conventions/SKILL.md -
Write the catalogue. The
nameis what everyone types after@, so choose it once and never change it.validate --strictfails without the top-leveldescription..claude-plugin/marketplace.json {"name": "acme-plugins","description": "Internal plugins for Acme engineering","owner": { "name": "Acme Platform Team", "email": "platform@acme.example" },"plugins": [{"name": "acme-release","source": "./plugins/acme-release","description": "Release checklist skill: changelog, migrations, feature flags"},{"name": "acme-api-style","source": "./plugins/acme-api-style","description": "Acme REST conventions: error envelope, pagination, versioning"}]}Do not put a
versionin these entries. If one disagrees withplugin.json,plugin.jsonwins at install time and the entry is silently ignored;validate --strictreports it as a warning and fails. -
Avoid reserved marketplace names. Claude Code refuses names that imitate Anthropic’s marketplaces. Observed on 2.1.283:
validate --strictpassed with"name": "claude-code-marketplace", and thenclaude plugin marketplace addfailed withThe name 'claude-code-marketplace' is reserved for official Anthropic marketplaces and can only be used with GitHub sources from the 'anthropics' organization.The same happened withanthropic-plugins. The marketplace reference also reservesnpm,github,gh,claudeai-*,inline,builtin,skills-dirandsynced. A prefix with your company name avoids all of them. -
Give each plugin a version. The
versioninplugin.jsonis the signal a teammate’s update acts on.plugins/acme-api-style/.claude-plugin/plugin.json {"name": "acme-api-style","version": "1.0.0","description": "Acme REST conventions: error envelope, pagination, versioning","author": { "name": "Acme Platform Team" }}plugins/acme-api-style/skills/api-conventions/SKILL.md ---name: api-conventionsdescription: Use when adding or changing an HTTP endpoint. Applies Acme's error envelope, cursor pagination and /v{n}/ versioning rules.---# Acme API conventions- Errors return {"error": {"code", "message", "request_id"}} with a 4xx or 5xx status.- List endpoints paginate with an opaque `cursor` and `limit` (max 100), never page numbers.- Breaking changes go in a new /v{n}/ prefix; never change a published response shape.acme-releasefollows the same shape with arelease-checkskill.The alternative is to omit
versioneverywhere. Claude Code then versions the plugin by the 12-character commit SHA, and every commit to the marketplace becomes a release (observed on 2.1.283:updated from 9235eac629d9 to b7aa77f81abc). That suits a marketplace where every merge should ship at once, butvalidate --strictthen fails withNo version specified, and teammates lose a readable number to report back. This page keeps explicit versions and enforces the bump in CI. -
Name an owner per plugin. A
CODEOWNERSfile makes the sign-off explicit..github/CODEOWNERS /plugins/acme-release/ @acme/release-eng/plugins/acme-api-style/ @acme/api-guild/.claude-plugin/ @acme/platform -
Push it to a private repository, for example
acme/acme-pluginson GitHub. GitLab and other hosts work through their fullhttps://…gitURL.
Validate the marketplace in CI before anyone installs from it
Section titled “Validate the marketplace in CI before anyone installs from it”claude plugin validate --strict is necessary but not sufficient. In the 2.1.283 test it passed a catalogue whose source pointed at a directory that did not exist, and it passed a reserved name. Both fail only when someone installs. The gate below therefore validates, then installs every plugin into a throwaway home directory with both CLIs, then enforces the version rule.
#!/usr/bin/env bash# Usage: scripts/check-marketplace.sh <base-ref> (CI passes origin/main)set -euo pipefailbase="${1:-origin/main}"market=$(jq -r .name .claude-plugin/marketplace.json)export HOME="$(mktemp -d)" CODEX_HOME="$(mktemp -d)" # scratch homes: nothing leaks in
# 1. Schema: --strict fails on warnings the runtime would tolerateclaude plugin validate --strict .for p in plugins/*/; do claude plugin validate --strict "$p"; done
# 2. Install smoke test: catches reserved names and missing source directories,# which validate passesclaude plugin marketplace add ./for name in $(jq -r '.plugins[].name' .claude-plugin/marketplace.json); do claude plugin install "$name@$market" claude plugin details "$name" | grep 'Always-on'donecodex plugin marketplace add ./for name in $(jq -r '.plugins[].name' .claude-plugin/marketplace.json); do codex plugin add "$name@$market"done
# 3. Release rule: a changed plugin must carry a new version, or nobody receives itfor p in plugins/*/; do git diff --quiet "$base" -- "$p" && continue old=$(git show "$base:${p}.claude-plugin/plugin.json" 2>/dev/null | jq -r .version || echo new) new=$(jq -r .version "${p}.claude-plugin/plugin.json") if [ "$old" = "$new" ]; then echo "::error::${p} changed but .claude-plugin/plugin.json still says $new" exit 1 fi # ...and the new version must sort above the old one (catches 1.0.0 -> 1.0) if [ "$old" != new ] && [ "$(printf '%s\n%s\n' "$old" "$new" | sort -V | tail -1)" != "$new" ]; then echo "::error::${p} version went from $old to $new; it must go up" exit 1 fidoneecho "marketplace OK"Run locally, the script printed Always-on: ~49 tok for acme-release (your figure depends on the skill description you write) and ~68 tok for acme-api-style and ended with marketplace OK. Codex prints a warning that it will not create helper binaries under a temporary directory; it is harmless in this job. With a mistyped source it stopped at Source path does not exist, and with a skill edited but not bumped it stopped at the ::error:: line. The second check also rejects a version that goes down or loses a component, such as 1.0.0 to 1.0. The version-bump step is our own rule, not a vendor feature.
The workflow holds no secrets, because validation and installation make no model call:
name: plugin-marketplaceon: pull_request: paths: ['plugins/**', '.claude-plugin/**', '.agents/**', '.cursor-plugin/**', 'scripts/**', '.github/**']permissions: contents: readjobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: fetch-depth: 0 persist-credentials: false - uses: actions/setup-node@v7 with: node-version: 22 - run: npm install -g @anthropic-ai/claude-code@2.1.283 @openai/codex@0.157.1 - run: bash scripts/check-marketplace.sh "origin/$BASE_REF" env: BASE_REF: ${{ github.base_ref }}Keep claude plugin eval, which calls a model and needs an API key, out of this pull_request job. Run it on the default branch or locally; building plugins shows how.
Release a plugin version and get it to teammates
Section titled “Release a plugin version and get it to teammates”Merging does not update anyone. Observed on 2.1.283: after a skill was edited without a version change, claude plugin marketplace update succeeded and claude plugin update acme-release@acme-plugins answered acme-release is already at the latest version (1.0.0); the cached skill still held the old text. After the bump to 1.0.1, the same command printed Plugin "acme-release" updated from 1.0.0 to 1.0.1 for scope user. Restart to apply changes. and wrote a new 1.0.1 cache directory beside the old one.
-
Tag the merged release. From a clean checkout of
main, the tag command checks thatplugin.jsonand the catalogue agree before it writes<name>--v<version>:Terminal window claude plugin tag --dry-run plugins/acme-release# Tag: acme-release--v1.0.1# √ Dry run — would create tag acme-release--v1.0.1 at HEAD in …claude plugin tag --push plugins/acme-releaseWhen a catalogue entry still carried
"version": "1.0.0", the command refused:Version mismatch: plugin.json says "1.0.1" but .claude-plugin/marketplace.json plugins[0].version says "1.0.0". -
Announce the release with the tag, the one-line change and the update commands below. Teammates update per tool:
Terminal window claude plugin marketplace update acme-pluginsclaude plugin update acme-release@acme-pluginsclaude plugin list # Version: 1.0.1Restart the session afterwards. Third-party marketplaces do not auto-update until a user or an admin turns it on (Claude Code 2.1.283 default; see the marketplaces reference), which is why the managed settings below set
autoUpdate.Terminal window codex plugin marketplace upgrade acme-pluginscodex plugin add acme-release@acme-pluginscodex plugin list # acme-release@acme-plugins installed, enabled 1.0.1Codex has no
codex plugin updateand nomarketplace update; re-runningaddinstalls the new version into its own cache directory (observed on 0.157.1).upgraderefreshes Git sources only: on a marketplace added from a local path it fails withmarketplace 'acme-plugins' is not configured as a Git marketplace.Cursor distributes a team marketplace from the dashboard rather than through a per-member command; how a new version reaches members is not verified here (cursor.com was unreachable on 2026-09-26). Check it in your dashboard after the first release.
-
Confirm the rollout. Ask the team to paste
claude plugin list(orcodex plugin list) output into the release thread, or check one machine per team. The release is done when the version you tagged is the version people run.
To undo a bad release, roll forward: revert the change on main, bump to a new version such as 1.0.2, and tag again. A new version number is the only thing an update acts on, so a revert that keeps 1.0.1 reaches nobody.
Roll the marketplace out to the whole team
Section titled “Roll the marketplace out to the whole team”Pick the route that matches how much control you need. Each one writes different settings, and none of them makes a private clone work without git credentials.
Private-repository access comes first. Claude Code clones with the machine’s git credentials and never prompts. For HTTPS on GitHub, run gh auth login and then gh auth setup-git. A GITHUB_TOKEN in the environment does nothing without a credential helper, and marketplace.json has no token field (checked on Claude Code 2.1.283). Set CLAUDE_CODE_PLUGIN_PREFER_HTTPS=1 to skip the SSH attempt. Test access with plain git before blaming the plugin: git ls-remote https://github.com/acme/acme-plugins.git.
| Route | Command or file | What it does |
|---|---|---|
| Per person | claude plugin marketplace add acme/acme-plugins, then claude plugin install acme-api-style@acme-plugins | Adds and installs at user scope |
| Per repository | claude plugin marketplace add acme/acme-plugins --scope project and claude plugin install acme-api-style@acme-plugins --scope project, then commit .claude/settings.json | Declares the marketplace in extraKnownMarketplaces and enables the plugin in enabledPlugins |
| Fleet-wide | Managed settings (server-managed, MDM or managed-settings.json) | Allowlists marketplaces, adds yours with auto-update, enables plugins |
| claude.ai Team or Enterprise | Organization settings → Plugins & skills | Syncs the marketplace through the org’s GitHub or GitLab connection; the repository must be private or internal, and no plugin may have a top-level bin/ directory |
The per-repository route writes both keys, but enabledPlugins enables a plugin; it does not install it. The marketplace registers only after each teammate trusts the folder, and a plugin with an external source still needs one claude plugin install … --scope project per machine. Put that command in the repository’s README, and have each teammate confirm the install with claude plugin list.
The fleet-wide route, adapted from Anthropic’s documented example (which also sets disableSideloadFlags). Keep { "source": "skills-dir" } in the allowlist: without it, strict mode also blocks the personal plugins that claude plugin init creates (<name>@skills-dir).
{ "strictKnownMarketplaces": [ { "source": "github", "repo": "anthropics/claude-plugins-official" }, { "source": "github", "repo": "acme/*" }, { "source": "skills-dir" } ], "extraKnownMarketplaces": { "acme-plugins": { "source": { "source": "github", "repo": "acme/acme-plugins" }, "autoUpdate": true } }, "enabledPlugins": { "acme-release@acme-plugins": true, "acme-api-style@acme-plugins": true }}strictKnownMarketplaces makes these the only marketplaces anyone can add. One policy for every coding agent covers how to distribute and audit that file.
Codex 0.157.1 reads .claude-plugin/marketplace.json when it is the repository’s only catalogue, so the example marketplace needs no Codex file. Observed:
codex plugin marketplace add acme/acme-plugins --ref maincodex plugin add acme-api-style@acme-pluginscodex plugin list# Marketplace `acme-plugins`# …/.claude-plugin/marketplace.json# acme-api-style@acme-plugins installed, enabled 1.0.0Codex records the result in config.toml as [marketplaces.acme-plugins] and [plugins."acme-api-style@acme-plugins"] enabled = true. It copies each plugin’s skills/ into plugins/cache/acme-plugins/<plugin>/<version>/; open a Codex session and run /skills to confirm the skills load before you announce support.
Add a Codex catalogue only when you need Codex-specific fields such as policy. When .agents/plugins/marketplace.json exists, Codex uses it instead of the Claude file, so the two lists must stay identical:
{ "name": "acme-plugins", "interface": { "displayName": "Acme engineering" }, "plugins": [ { "name": "acme-release", "source": { "source": "local", "path": "./plugins/acme-release" }, "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" }, "category": "Developer Tools" }, { "name": "acme-api-style", "source": { "source": "local", "path": "./plugins/acme-api-style" }, "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" }, "category": "Developer Tools" } ]}If you add it, add one line to the CI script: diff <(jq -r '.plugins[].name' .claude-plugin/marketplace.json | sort) <(jq -r '.plugins[].name' .agents/plugins/marketplace.json | sort).
Cursor needs its own catalogue at .cursor-plugin/marketplace.json, plus a .cursor-plugin/plugin.json in each plugin directory. The skills in skills/ are reused as they are. The files below pass the JSON schemas and the scripts/validate-plugins.mjs checker in Cursor’s first-party cursor/plugins repository (commit of 2026-09-25):
{ "name": "acme-plugins", "owner": { "name": "Acme Platform Team", "email": "platform@acme.example" }, "metadata": { "description": "Internal plugins for Acme engineering" }, "plugins": [ { "name": "acme-release", "source": "plugins/acme-release", "description": "Release checklist skill: changelog, migrations, feature flags" }, { "name": "acme-api-style", "source": "plugins/acme-api-style", "description": "Acme REST conventions: error envelope, pagination, versioning" } ]}{ "name": "acme-api-style", "version": "1.0.0", "description": "Acme REST conventions: error envelope, pagination, versioning", "author": { "name": "Acme Platform Team" }, "skills": "./skills/"}Do not copy the Claude catalogue across. Cursor’s schema forbids unknown top-level keys, so the Claude file’s top-level description fails with must NOT have additional properties; Cursor expects it under metadata. To gate it in CI, copy schemas/ and scripts/validate-plugins.mjs from cursor/plugins, install ajv and ajv-formats, and run node scripts/validate-plugins.mjs. It also fails on a missing source directory. Keep both plugin.json files of a plugin at the same version; the CI version rule above checks only the Claude one, so add the Cursor path to it if Cursor users matter to you.
The import is secondary evidence (Cursor’s documentation was unreachable on 2026-09-26): on a Teams or Enterprise plan, an admin opens Dashboard → Settings → Plugins → Team Marketplaces → Import, pastes the repository URL, chooses access groups, and marks each plugin Required or Optional. Members install with /add-plugin in Agent chat. A community template’s README says the Teams plan gets one team marketplace; confirm the limit in your dashboard.
Headless agents in CI or containers need the plugins too. Build a seed directory once with CLAUDE_CODE_PLUGIN_CACHE_DIR=/opt/claude-seed claude plugin install …, run the agent with CLAUDE_CODE_PLUGIN_SEED_DIR=/opt/claude-seed, and set CLAUDE_CODE_SYNC_PLUGIN_INSTALL=1 so a -p run waits for installs. Headless agents in CI covers the rest of that setup.
How big public plugin marketplaces are, and what a team plugin costs in context
Section titled “How big public plugin marketplaces are, and what a team plugin costs in context”As of 2026-09-26, Anthropic’s claude-plugins-official catalogue listed 314 entries, Codex’s openai-curated 65 and Cursor’s cursor-plugins 94 (counted from each repository’s marketplace file). The largest cross-agent third-party marketplace in our research, wshobson/agents (marketplace name claude-code-workflows), shipped 94 plugins with both a Claude and a Codex catalogue and had 39,978 GitHub stars (GitHub API, 2026-09-26). Private marketplaces have no public count.
The two example plugins cost 49 and 68 always-on tokens per session (claude plugin details, 2.1.283), about 117 together. Skill-only plugins stay cheap; a plugin that bundles many skills or an MCP server costs more, and on 2.1.283 the figure from claude plugin details may not include MCP tool schemas, so check /context in a session as well. Print the always-on figure in CI, as the script does, and treat a jump in review as a design question.
What breaks in a team marketplace, and how to recover
Section titled “What breaks in a team marketplace, and how to recover”- Teammates still run the old skill after a merge. The version did not change, or they never updated. Bump
version, tag, and send the update commands; check withclaude plugin list. Add the version-bump step to CI so it cannot happen twice. marketplace addfails with “reserved for official Anthropic marketplaces”. Rename the catalogue before anyone installs it. After people have installed, a rename changes every install id, so treat it as a migration: announce it, and have everyone remove the old marketplace and add the new one.- You have to rename a plugin inside the marketplace. Do not just change its
name. Add the old name to the catalogue’srenamesmap ({ "old-name": "new-name" }, ornullfor a removed plugin). Afterclaude plugin marketplace update, Claude Code 2.1.283 moves each teammate’senabledPluginsentry to the new name, andclaude plugin listmarks the old install “Renamed to …”. It does not resolve the old id:installandupdateby the old name fail with “not found”, so teammates runclaude plugin install <new-name>@acme-plugins. Keep old entries in the map. - Install fails with
Source path does not exist. A plugin directory was moved or renamed without itssource. Fix the entry; the CI install step catches the next one. - The marketplace never appears on a teammate’s machine. They have not trusted the folder, or their git credentials cannot clone the private repository. Run
git ls-remoteon the repository URL; if it prompts or fails, fixgh auth setup-git(or your host’s credential helper) first. - A committed plugin is enabled but missing.
enabledPluginsdoes not install. Runclaude plugin install <name>@acme-plugins --scope projecton that machine. - Codex installs a different plugin list from Claude Code. A
.agents/plugins/marketplace.jsonexists and Codex reads it instead. Make the two lists identical, and add thediffline to CI. codex plugin marketplace upgradefails with “not configured as a Git marketplace”. The marketplace was added from a local path. Remove it and add it again fromacme/acme-plugins.- Cursor’s validator reports
must NOT have additional properties. The Cursor catalogue was copied from the Claude one. Movedescriptionundermetadataand drop any other Claude-only key. claude plugin tagrefuses with a version mismatch. A catalogue entry carries its ownversion. Delete that field and tag again.