Skip to content

Authoring a marketplace

This guide is for marketplace maintainers — people who curate a set of plugin packages for their team or organisation. If you are a consumer installing plugins from an existing marketplace, see the Marketplaces guide instead.

APM uses a single source-of-truth model:

  • apm.yml — your project manifest, hand-edited. A top-level marketplace: block declares the marketplace.
  • .claude-plugin/marketplace.json — compiled artefact, byte-for-byte compliant with Anthropic’s marketplace.json specification. Consumed by Claude Code, Copilot CLI, and APM itself.

Both files are committed. apm.yml is edited; marketplace.json is regenerated by apm pack.

Terminal window
# 1. Add a marketplace block to your existing apm.yml
apm marketplace init
# 2. Edit the block in apm.yml -- describe each plugin under marketplace.plugins
$EDITOR apm.yml
# 3. Build the marketplace
apm pack
# 4. Commit both files
git add apm.yml .claude-plugin/marketplace.json
git commit -m "Initial marketplace"
git push

apm marketplace init appends a richly commented marketplace: block to your existing apm.yml and creates an empty .claude-plugin/ directory. It does NOT create a standalone marketplace.yml. If your project has no apm.yml yet, run apm init first.

apm pack is the universal build verb. When apm.yml contains a marketplace: block, it writes .claude-plugin/marketplace.json. When apm.yml also contains dependencies:, it writes a bundle to ./build/<name>/ in the same run. The manifest drives what gets produced — there is no separate “marketplace build” command.

Consumers register your repository with apm marketplace add <owner>/<repo> and install packages from it.

Real-world example: microsoft/azure-skills

Section titled “Real-world example: microsoft/azure-skills”

The microsoft/azure-skills repository ships an apm.yml plus a hand-authored .claude-plugin/marketplace.json. Running apm pack against its apm.yml produces a byte-for-byte identical marketplace.json — proof that the marketplace: block fully expresses the Anthropic shape.

apm.yml
name: azure-skills
version: 1.0.0
description: Microsoft Azure MCP and Skills integration
marketplace:
owner:
name: Microsoft
url: https://www.microsoft.com
plugins:
- name: azure
description: Microsoft Azure MCP integration for cloud resource management
source: ./.github/plugins/azure-skills
homepage: https://github.com/microsoft/azure-skills

Note three things:

  • No name, description, or version inside marketplace: — they are inherited from the apm.yml top level.
  • source: ./.github/plugins/azure-skills is a local-path entry: the plugin lives in this same repo.
  • No tags: — empty/absent tags are omitted from marketplace.json to match Anthropic’s canonical shape.

Build it:

$ apm pack
[+] Built marketplace.json (1 plugins) -> .claude-plugin/marketplace.json

Full example with both remote and local plugins:

name: my-project
version: 1.2.0
description: Curated plugins for the acme-org engineering team
marketplace:
# Optional overrides. Omit to inherit from apm.yml top level.
# name: my-marketplace
# description: ...
# version: 1.2.0
owner:
name: acme-org
url: https://github.com/acme-org
email: maintainers@acme-org.example
# APM-only: stripped from marketplace.json at compile time.
build:
tagPattern: "v{version}"
# Pass-through: copied verbatim into marketplace.json.
metadata:
homepage: https://example.com/plugins
pluginRoot: ./plugins
plugins:
- name: example-plugin
description: Example plugin consumers will see
source: acme-org/example-plugin
version: "^1.0.0"
- name: monorepo-tool
description: Plugin that lives in a subdirectory
source: acme-org/monorepo
subdir: tools/monorepo-tool
version: "~2.3.0"
tag_pattern: "monorepo-tool-v{version}"
- name: pinned-plugin
description: Pinned to an explicit ref
source: acme-org/pinned-plugin
ref: 3f2a9b1c
- name: local-tool
description: Plugin shipped alongside this repo
source: ./plugins/local-tool
version: 0.1.0
FieldRequiredDescription
namenoOverride the apm.yml top-level name. Inherited when omitted.
descriptionnoOverride the top-level description. Inherited when omitted.
versionnoOverride the top-level version. Inherited when omitted.
owneryesMapping with name (required), optional url, email.
buildnoAPM-only build options. See below.
metadatanoOpaque pass-through copied into marketplace.json.
pluginsnoList of plugin entries.

When name/description/version are inherited (not overridden), they are also omitted from the generated marketplace.json top level so the artefact stays stable across unrelated bumps to apm.yml.

FieldDefaultDescription
tagPatternv{version}Marketplace-wide default for resolving {version} to a git tag. Accepts {version} and {name} placeholders.

Stripped from marketplace.json at compile time.

FieldRequiredDescription
nameyesPlugin name consumers will install. Unique within the marketplace.
sourceyesEither <owner>/<repo> (remote) or ./path/to/dir (local-path entry in this repo).
descriptionnoPass-through to marketplace.json. For remote entries, overrides the remote-fetched description (curator-wins).
homepagenoPass-through URL.
tagsnoPass-through list of strings. Omitted from output when empty. Max 50 items, 100 chars each.
keywordsnoAlias for tags. Merged with tags (deduplicated). Same limits apply to the combined list.
authornoPass-through string (e.g. "ACME Corp"). Must be a string if set.
licensenoPass-through string (e.g. "MIT"). Must be a string if set.
repositorynoPass-through URL string (e.g. "https://github.com/org/repo"). Must be a string if set.
versionconditionalSemver range (see below). Either version or ref must be set for remote sources. For remote entries, a fixed version (not a range) is emitted as display metadata (curator-wins override). Local sources may set version to seed the compiled output.
refconditionalExplicit SHA, tag, or branch. Takes precedence over version. Remote sources only.
subdirnoSubdirectory within a remote repo. Validated against path traversal.
tag_patternnoPer-plugin override of build.tagPattern.
include_prereleasenoInclude semver pre-release tags in range resolution. Defaults to false.

Unknown keys inside marketplace: raise a schema error rather than being silently ignored.

When source starts with ./, the entry is a local-path plugin: APM does not run git ls-remote, does not resolve a SHA, and emits the path into marketplace.json as a plain string source.

If metadata.pluginRoot is set, local source paths are emitted relative to pluginRoot in the output. For example, with pluginRoot: ./plugins, a source of ./plugins/my-tool is emitted as ./my-tool. This prevents double-prefix bugs where consumers prepend pluginRoot to an already-rooted path.

If the source path does not start with pluginRoot, it is emitted verbatim and a build warning is produced.

plugins:
- name: local-tool
source: ./plugins/local-tool
description: Vendored alongside this marketplace
version: 0.1.0

Both apm.yml and the generated .claude-plugin/marketplace.json must be tracked. apm marketplace init warns if your .gitignore would exclude the generated file. If you use a generic *.json rule, add an explicit unignore:

.gitignore
*.json
!.claude-plugin/marketplace.json

apm pack reads apm.yml, resolves each remote plugin against git ls-remote, leaves local-path entries untouched, and writes .claude-plugin/marketplace.json atomically (temp file plus rename).

$ apm pack

Marketplace-relevant flags:

FlagDescription
--dry-runResolve and print the result table, but do not write marketplace.json.
--offlineUse only cached refs; fail entries that need a fresh git ls-remote.
--include-prereleaseAllow pre-release tags to satisfy every range (overrides per-entry flag).
--marketplace-output PATHOverride the output path. Default: .claude-plugin/marketplace.json.
-v, --verboseInclude per-entry resolution detail.

apm pack also accepts bundle flags (--format, --target, --archive, -o, --force); they are silent no-ops in a marketplace-only project. See apm pack reference for the full list.

The default output path matches Anthropic’s convention — Claude Code reads .claude-plugin/marketplace.json from the repo root. Override with --marketplace-output PATH only when you need to inspect a build without touching the committed file:

Terminal window
apm pack --marketplace-output ./build/marketplace.json --dry-run
  1. Parses and validates the marketplace: block. Unknown keys or invalid semver is a schema error (exit 2).
  2. For each remote plugin: runs git ls-remote, enumerates tags and branches, filters by the entry’s tag pattern, resolves the version range, picks the highest match.
  3. For each local-path plugin: emits the path verbatim, no resolution.
  4. Walks metadata: unchanged into the output.
  5. Emits plugins: with the Anthropic key name; each entry carries the resolved source plus any pass-through fields. Inherited top-level fields and empty tags: are omitted.
  6. Writes the file atomically.
CodeMeaning
0Build succeeded; marketplace.json written (or previewed).
1Build error — network failure, ref not found, no tag matches the range, etc.
2Schema error in the marketplace: block.

marketplace.json produced by apm pack follows three rules:

  1. plugins: is emitted verbatim. APM does not rename, reorder, or decorate plugin entries.
  2. metadata: is an opaque pass-through. Whatever you put under marketplace.metadata: in apm.yml is copied byte-for-byte into marketplace.json, preserving key casing (for example, pluginRoot stays pluginRoot).
  3. APM-only fields are stripped at compile time. The build: block, per-plugin version ranges, tag_pattern overrides, and include_prerelease flags live only in apm.yml. They never leak into marketplace.json.

APM does not emit a versions[] array. Each compiled plugin has exactly one resolved source.ref — the latest commit SHA (or explicit ref) that satisfies the declared range at build time. Empty tags: and inherited description/version are omitted from output.

Earlier APM versions stored this configuration in a standalone marketplace.yml. That file is deprecated. APM still reads it (with a warning) when no marketplace: block is present in apm.yml, but apm marketplace init no longer creates one. Both files present at once is a hard error.

Run the one-shot migration:

Terminal window
apm marketplace migrate # preview the new apm.yml block
apm marketplace migrate --yes # apply: rewrite apm.yml, delete marketplace.yml

--force, --yes, and -y are equivalent overrides for an existing marketplace: block in apm.yml. After migration, commit apm.yml (and the deleted marketplace.yml).

APM uses npm-compatible semver ranges. The most common forms:

RangeMatches
1.2.3Exact version.
^1.2.3Compatible: >=1.2.3 <2.0.0.
~1.2.3Patch-level: >=1.2.3 <1.3.0.
>=1.2.0Everything from 1.2.0 upwards.
<2.0.0Everything below 2.0.0.
1.x or 1.*Any 1.y.z.
>=1.2.0 <2.0.0AND-combination.

Pre-release tags (for example 1.2.0-beta.1) are excluded by default. Set include_prerelease: true on the entry, or pass --include-prerelease to apm pack, to include them.

Pin to a non-semver ref when you need exact reproducibility:

plugins:
- name: pinned-plugin
source: acme-org/pinned-plugin
ref: 3f2a9b1cdeadbeef # SHA, tag, or branch -- overrides version ranges

ref takes precedence over version. If both are set, version is ignored.

Three subcommands let you manage entries in marketplace.plugins without hand-editing YAML.

Terminal window
apm marketplace package add microsoft/apm-sample-package \
--version ">=1.0.0" \
--description "Sample package"

package add takes a <owner>/<repo> source, derives the plugin name from the repo, and appends an entry to marketplace.plugins in apm.yml. Pass --name to override the derived name, --subdir for monorepo paths, --tag-pattern for non-default tag layouts, or --tags to attach metadata tags. By default the command verifies the source is reachable via git ls-remote; pass --no-verify to skip that check.

--version and --ref are mutually exclusive — use --ref to pin an exact SHA, tag, or branch instead of a semver range.

Terminal window
apm marketplace package set apm-sample-package --version ">=2.0.0"

package set takes the plugin name (not the source) and updates the specified fields in place.

Terminal window
apm marketplace package remove apm-sample-package --yes

package remove drops the named entry. Without --yes the command prompts for confirmation.

Validates the schema and verifies every entry is resolvable. Use it in CI before publishing.

Terminal window
apm marketplace check
apm marketplace check --offline # schema + cached refs only

Exit code is non-zero when any entry is unreachable, a ref does not exist, or no tag satisfies a range.

Checks the environment — git version, network reachability of common hosts, gh CLI presence, git authentication, and whether the project’s marketplace config is present and parses.

Terminal window
apm marketplace doctor

Run it first when apm pack or publish fails in an unfamiliar environment.

SymptomCauseFix
Both apm.yml ... and marketplace.yml existLegacy file lingered after edits to apm.yml.Run apm marketplace migrate --yes (or delete marketplace.yml if apm.yml is already the source of truth).
'plugins[0].source' must match ...source is a full URL or contains a path.Use owner/repo, or ./path for a local entry, and put repo paths under subdir:.
No tag matching '^1.0.0'No published tags satisfy the range under your tag pattern.Loosen the range, check tag_pattern, or pin with ref:.
Ref 'main' not foundBranch or tag does not exist upstream.Verify with git ls-remote <url>.
Pre-release tags skippedLatest published tag is a pre-release.Set include_prerelease: true on the entry or pass --include-prerelease.
No cached refs (offline)First-ever --offline build.Run once online to populate the cache, then retry offline.
git ls-remote auth failurePrivate source without credentials.Ensure your git credentials (SSH agent or gh auth login) can reach the source repo.

apm pack respects the GITHUB_HOST environment variable. Set it before building to resolve plugins from a GHES instance:

Terminal window
export GITHUB_HOST=github.company.com
apm pack

Token resolution and metadata fetch use the same host, so existing auth configuration (see Authentication) works automatically. git ls-remote calls are authenticated with the resolved token, so private GHES repos work without a separate git credential helper.

apm marketplace outdated compares the currently resolved version of each plugin (as captured in marketplace.json) against the latest tag available in the source repo.

Terminal window
apm marketplace outdated
apm marketplace outdated --include-prerelease
apm marketplace outdated --offline

Output columns: plugin, current version, declared range, latest in range, latest overall. Plugins whose “latest overall” exceeds “latest in range” need a manual range bump (for example, widening ^1.0.0 to ^2.0.0) before the next apm pack will pick them up. This is intentional — major-version bumps are a maintainer decision.

Plugins pinned with ref: and local-path entries show -- in the range columns; outdated cannot reason about them.

apm marketplace publish drives the compiled marketplace.json out to consumer repositories and opens pull requests on their behalf. It is the end-to-end flow for “I just built a new marketplace version; roll it out.”

You need:

  1. A built marketplace.json on the current branch (run apm pack first).
  2. A consumer-targets.yml file listing the repos to update.
  3. The gh CLI authenticated against GitHub (unless you use --no-pr).
consumer-targets.yml
targets:
- repo: acme-org/service-a
branch: main
- repo: acme-org/service-b
branch: develop
path_in_repo: apm/apm.yml # optional; defaults to apm.yml
- repo: acme-org/service-c
branch: main

repo and branch are required; path_in_repo defaults to apm.yml. Paths are validated for traversal.

Always dry-run first:

Terminal window
apm marketplace publish --dry-run --yes

This clones each target, computes what would change in its lockfile references, and prints a plan. Nothing is pushed.

Terminal window
apm marketplace publish

Output shows per-target status: updated, unchanged, failed. PR URLs are printed for each target that had changes.

FlagPurpose
--targets PATHUse a custom targets file (default ./consumer-targets.yml).
--dry-runPreview; no push, no PR.
--no-prPush the branch to each target but skip PR creation.
--draftOpen PRs as drafts.
--allow-downgradeAllow pushing a lower version than the target currently references.
--allow-ref-changeAllow switching ref types (for example, branch to SHA).
--parallel NMaximum concurrent targets. Default 4.
--yes, -ySkip interactive confirmation (required for non-interactive CI).
-v, --verbosePer-target detail.

Publish runs append to .apm/publish-state.json, which records the history of runs (timestamps, targets, outcomes, PR URLs). This lets later invocations detect already-open PRs and avoid opening duplicates. The file is safe to commit or to gitignore — it is advisory, not authoritative.

Projects that prefix tags with a plugin name (common in monorepos) need a per-entry pattern:

marketplace:
plugins:
- name: ui-components
source: acme-org/frontend-monorepo
subdir: packages/ui-components
version: "^3.0.0"
tag_pattern: "ui-components-v{version}"

The {name} placeholder resolves to the plugin entry’s name, so you can also write tag_pattern: "{name}-v{version}" and reuse a single build.tagPattern.

Set include_prerelease: true on the entry, or pass --include-prerelease to apm pack and apm marketplace outdated for the whole marketplace:

marketplace:
plugins:
- name: example-plugin
source: acme-org/example-plugin
version: ">=1.0.0-0"
include_prerelease: true

Note the -0 pre-release suffix on the range — it makes the lower bound inclusive of pre-releases.

Not in the first release. apm marketplace publish uses the gh CLI and assumes GitHub for PR creation. You can still pack and check against any git remote that speaks git ls-remote over HTTPS or SSH; only publish is GitHub-specific. For non-GitHub consumers, run publish --no-pr and drive PR creation through your own tooling.