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-levelmarketplace: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.
Quickstart
Section titled “Quickstart”# 1. Add a marketplace block to your existing apm.ymlapm marketplace init
# 2. Edit the block in apm.yml -- describe each plugin under marketplace.plugins$EDITOR apm.yml
# 3. Build the marketplaceapm pack
# 4. Commit both filesgit add apm.yml .claude-plugin/marketplace.jsongit commit -m "Initial marketplace"git pushapm 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.
name: azure-skillsversion: 1.0.0description: 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-skillsNote three things:
- No
name,description, orversioninsidemarketplace:— they are inherited from theapm.ymltop level. source: ./.github/plugins/azure-skillsis a local-path entry: the plugin lives in this same repo.- No
tags:— empty/absent tags are omitted frommarketplace.jsonto match Anthropic’s canonical shape.
Build it:
$ apm pack[+] Built marketplace.json (1 plugins) -> .claude-plugin/marketplace.jsonThe marketplace: schema
Section titled “The marketplace: schema”Full example with both remote and local plugins:
name: my-projectversion: 1.2.0description: 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.0Fields inside marketplace:
Section titled “Fields inside marketplace:”| Field | Required | Description |
|---|---|---|
name | no | Override the apm.yml top-level name. Inherited when omitted. |
description | no | Override the top-level description. Inherited when omitted. |
version | no | Override the top-level version. Inherited when omitted. |
owner | yes | Mapping with name (required), optional url, email. |
build | no | APM-only build options. See below. |
metadata | no | Opaque pass-through copied into marketplace.json. |
plugins | no | List 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.
The build block (APM-only)
Section titled “The build block (APM-only)”| Field | Default | Description |
|---|---|---|
tagPattern | v{version} | Marketplace-wide default for resolving {version} to a git tag. Accepts {version} and {name} placeholders. |
Stripped from marketplace.json at compile time.
Plugin entries
Section titled “Plugin entries”| Field | Required | Description |
|---|---|---|
name | yes | Plugin name consumers will install. Unique within the marketplace. |
source | yes | Either <owner>/<repo> (remote) or ./path/to/dir (local-path entry in this repo). |
description | no | Pass-through to marketplace.json. For remote entries, overrides the remote-fetched description (curator-wins). |
homepage | no | Pass-through URL. |
tags | no | Pass-through list of strings. Omitted from output when empty. Max 50 items, 100 chars each. |
keywords | no | Alias for tags. Merged with tags (deduplicated). Same limits apply to the combined list. |
author | no | Pass-through string (e.g. "ACME Corp"). Must be a string if set. |
license | no | Pass-through string (e.g. "MIT"). Must be a string if set. |
repository | no | Pass-through URL string (e.g. "https://github.com/org/repo"). Must be a string if set. |
version | conditional | Semver 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. |
ref | conditional | Explicit SHA, tag, or branch. Takes precedence over version. Remote sources only. |
subdir | no | Subdirectory within a remote repo. Validated against path traversal. |
tag_pattern | no | Per-plugin override of build.tagPattern. |
include_prerelease | no | Include semver pre-release tags in range resolution. Defaults to false. |
Unknown keys inside marketplace: raise a schema error rather than being silently ignored.
Local-path entries
Section titled “Local-path entries”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.gitignore
Section titled “.gitignore”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:
*.json!.claude-plugin/marketplace.jsonThe build flow
Section titled “The build flow”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 packMarketplace-relevant flags:
| Flag | Description |
|---|---|
--dry-run | Resolve and print the result table, but do not write marketplace.json. |
--offline | Use only cached refs; fail entries that need a fresh git ls-remote. |
--include-prerelease | Allow pre-release tags to satisfy every range (overrides per-entry flag). |
--marketplace-output PATH | Override the output path. Default: .claude-plugin/marketplace.json. |
-v, --verbose | Include 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:
apm pack --marketplace-output ./build/marketplace.json --dry-runWhat the compiler does
Section titled “What the compiler does”- Parses and validates the
marketplace:block. Unknown keys or invalid semver is a schema error (exit 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. - For each local-path plugin: emits the path verbatim, no resolution.
- Walks
metadata:unchanged into the output. - Emits
plugins:with the Anthropic key name; each entry carries the resolvedsourceplus any pass-through fields. Inherited top-level fields and emptytags:are omitted. - Writes the file atomically.
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
0 | Build succeeded; marketplace.json written (or previewed). |
1 | Build error — network failure, ref not found, no tag matches the range, etc. |
2 | Schema error in the marketplace: block. |
Anthropic compliance
Section titled “Anthropic compliance”marketplace.json produced by apm pack follows three rules:
plugins:is emitted verbatim. APM does not rename, reorder, or decorate plugin entries.metadata:is an opaque pass-through. Whatever you put undermarketplace.metadata:inapm.ymlis copied byte-for-byte intomarketplace.json, preserving key casing (for example,pluginRootstayspluginRoot).- APM-only fields are stripped at compile time. The
build:block, per-pluginversionranges,tag_patternoverrides, andinclude_prereleaseflags live only inapm.yml. They never leak intomarketplace.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.
Migrating from marketplace.yml
Section titled “Migrating from marketplace.yml”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:
apm marketplace migrate # preview the new apm.yml blockapm 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).
Version ranges
Section titled “Version ranges”APM uses npm-compatible semver ranges. The most common forms:
| Range | Matches |
|---|---|
1.2.3 | Exact version. |
^1.2.3 | Compatible: >=1.2.3 <2.0.0. |
~1.2.3 | Patch-level: >=1.2.3 <1.3.0. |
>=1.2.0 | Everything from 1.2.0 upwards. |
<2.0.0 | Everything below 2.0.0. |
1.x or 1.* | Any 1.y.z. |
>=1.2.0 <2.0.0 | AND-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 rangesref takes precedence over version. If both are set, version is ignored.
Managing plugins
Section titled “Managing plugins”Three subcommands let you manage entries in marketplace.plugins without hand-editing YAML.
Adding a plugin
Section titled “Adding a plugin”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.
Updating a plugin
Section titled “Updating a plugin”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.
Removing a plugin
Section titled “Removing a plugin”apm marketplace package remove apm-sample-package --yespackage remove drops the named entry. Without --yes the command prompts for confirmation.
Checking and troubleshooting
Section titled “Checking and troubleshooting”apm marketplace check
Section titled “apm marketplace check”Validates the schema and verifies every entry is resolvable. Use it in CI before publishing.
apm marketplace checkapm marketplace check --offline # schema + cached refs onlyExit code is non-zero when any entry is unreachable, a ref does not exist, or no tag satisfies a range.
apm marketplace doctor
Section titled “apm marketplace doctor”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.
apm marketplace doctorRun it first when apm pack or publish fails in an unfamiliar environment.
Common errors
Section titled “Common errors”| Symptom | Cause | Fix |
|---|---|---|
Both apm.yml ... and marketplace.yml exist | Legacy 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 found | Branch or tag does not exist upstream. | Verify with git ls-remote <url>. |
Pre-release tags skipped | Latest 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 failure | Private source without credentials. | Ensure your git credentials (SSH agent or gh auth login) can reach the source repo. |
GitHub Enterprise Server
Section titled “GitHub Enterprise Server”apm pack respects the GITHUB_HOST environment variable. Set it before building to resolve plugins from a GHES instance:
export GITHUB_HOST=github.company.comapm packToken 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.
Discovering upgrades
Section titled “Discovering upgrades”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.
apm marketplace outdatedapm marketplace outdated --include-prereleaseapm marketplace outdated --offlineOutput 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.
Publishing to consumers
Section titled “Publishing to consumers”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:
- A built
marketplace.jsonon the current branch (runapm packfirst). - A
consumer-targets.ymlfile listing the repos to update. - The
ghCLI authenticated against GitHub (unless you use--no-pr).
The targets file
Section titled “The targets file”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: mainrepo and branch are required; path_in_repo defaults to apm.yml. Paths are validated for traversal.
First run — preview
Section titled “First run — preview”Always dry-run first:
apm marketplace publish --dry-run --yesThis clones each target, computes what would change in its lockfile references, and prints a plan. Nothing is pushed.
Real run
Section titled “Real run”apm marketplace publishOutput shows per-target status: updated, unchanged, failed. PR URLs are printed for each target that had changes.
Useful flags
Section titled “Useful flags”| Flag | Purpose |
|---|---|
--targets PATH | Use a custom targets file (default ./consumer-targets.yml). |
--dry-run | Preview; no push, no PR. |
--no-pr | Push the branch to each target but skip PR creation. |
--draft | Open PRs as drafts. |
--allow-downgrade | Allow pushing a lower version than the target currently references. |
--allow-ref-change | Allow switching ref types (for example, branch to SHA). |
--parallel N | Maximum concurrent targets. Default 4. |
--yes, -y | Skip interactive confirmation (required for non-interactive CI). |
-v, --verbose | Per-target detail. |
State file
Section titled “State file”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.
Recipes
Section titled “Recipes”Custom tag pattern
Section titled “Custom tag pattern”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.
Pre-release tags are being skipped
Section titled “Pre-release tags are being skipped”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: trueNote the -0 pre-release suffix on the range — it makes the lower bound inclusive of pre-releases.
Can I use a non-GitHub host?
Section titled “Can I use a non-GitHub host?”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.
Related reading
Section titled “Related reading”- Marketplaces guide — consumer-side: registering and installing from a marketplace.
- CLI command reference — authoritative options for
apm packand everyapm marketplacesubcommand. - Manifest schema — the
apm.ymlshape including themarketplace:block. - Plugins guide — what a plugin is and how consumers install one.