Skip to content

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.

  • 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.

StageWhoCheck that proves itTool
Pull requestPlugin authorCI: strict validation, install smoke test, version-bump rulescripts/check-marketplace.sh
ReviewOwner in CODEOWNERSCI green, version bumped, always-on token cost read from CI logGitHub review
ReleaseMarketplace ownerclaude plugin tag confirms plugin.json and the catalogue agreeclaude plugin tag --push
RolloutEvery teammate, or managed settingsclaude plugin list shows the new versionclaude 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.

  1. 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
  2. Write the catalogue. The name is what everyone types after @, so choose it once and never change it. validate --strict fails without the top-level description.

    .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 version in these entries. If one disagrees with plugin.json, plugin.json wins at install time and the entry is silently ignored; validate --strict reports it as a warning and fails.

  3. Avoid reserved marketplace names. Claude Code refuses names that imitate Anthropic’s marketplaces. Observed on 2.1.283: validate --strict passed with "name": "claude-code-marketplace", and then claude plugin marketplace add failed with The 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 with anthropic-plugins. The marketplace reference also reserves npm, github, gh, claudeai-*, inline, builtin, skills-dir and synced. A prefix with your company name avoids all of them.

  4. Give each plugin a version. The version in plugin.json is 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-conventions
    description: 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-release follows the same shape with a release-check skill.

    The alternative is to omit version everywhere. 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, but validate --strict then fails with No version specified, and teammates lose a readable number to report back. This page keeps explicit versions and enforces the bump in CI.

  5. Name an owner per plugin. A CODEOWNERS file makes the sign-off explicit.

    .github/CODEOWNERS
    /plugins/acme-release/ @acme/release-eng
    /plugins/acme-api-style/ @acme/api-guild
    /.claude-plugin/ @acme/platform
  6. Push it to a private repository, for example acme/acme-plugins on GitHub. GitLab and other hosts work through their full https://…git URL.

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.

scripts/check-marketplace.sh
#!/usr/bin/env bash
# Usage: scripts/check-marketplace.sh <base-ref> (CI passes origin/main)
set -euo pipefail
base="${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 tolerate
claude 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 passes
claude 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'
done
codex 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 it
for 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
fi
done
echo "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:

.github/workflows/plugin-marketplace.yml
name: plugin-marketplace
on:
pull_request:
paths: ['plugins/**', '.claude-plugin/**', '.agents/**', '.cursor-plugin/**', 'scripts/**', '.github/**']
permissions:
contents: read
jobs:
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.

  1. Tag the merged release. From a clean checkout of main, the tag command checks that plugin.json and 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-release

    When 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".

  2. 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-plugins
    claude plugin update acme-release@acme-plugins
    claude plugin list # Version: 1.0.1

    Restart 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.

  3. Confirm the rollout. Ask the team to paste claude plugin list (or codex 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.

RouteCommand or fileWhat it does
Per personclaude plugin marketplace add acme/acme-plugins, then claude plugin install acme-api-style@acme-pluginsAdds and installs at user scope
Per repositoryclaude plugin marketplace add acme/acme-plugins --scope project and claude plugin install acme-api-style@acme-plugins --scope project, then commit .claude/settings.jsonDeclares the marketplace in extraKnownMarketplaces and enables the plugin in enabledPlugins
Fleet-wideManaged settings (server-managed, MDM or managed-settings.json)Allowlists marketplaces, adds yours with auto-update, enables plugins
claude.ai Team or EnterpriseOrganization settings → Plugins & skillsSyncs 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).

managed-settings.json
{
"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.

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 with claude plugin list. Add the version-bump step to CI so it cannot happen twice.
  • marketplace add fails 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’s renames map ({ "old-name": "new-name" }, or null for a removed plugin). After claude plugin marketplace update, Claude Code 2.1.283 moves each teammate’s enabledPlugins entry to the new name, and claude plugin list marks the old install “Renamed to …”. It does not resolve the old id: install and update by the old name fail with “not found”, so teammates run claude 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 its source. 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-remote on the repository URL; if it prompts or fails, fix gh auth setup-git (or your host’s credential helper) first.
  • A committed plugin is enabled but missing. enabledPlugins does not install. Run claude plugin install <name>@acme-plugins --scope project on that machine.
  • Codex installs a different plugin list from Claude Code. A .agents/plugins/marketplace.json exists and Codex reads it instead. Make the two lists identical, and add the diff line to CI.
  • codex plugin marketplace upgrade fails with “not configured as a Git marketplace”. The marketplace was added from a local path. Remove it and add it again from acme/acme-plugins.
  • Cursor’s validator reports must NOT have additional properties. The Cursor catalogue was copied from the Claude one. Move description under metadata and drop any other Claude-only key.
  • claude plugin tag refuses with a version mismatch. A catalogue entry carries its own version. Delete that field and tag again.