Skip to content

Publish to a marketplace

A marketplace in APM is a curated index of packages that one repo publishes and many repos install from. You author it as a marketplace: block in apm.yml, build it into one or more marketplace artifacts with apm pack, and let consumers register your repo with apm marketplace add. This page covers the authoring surface: the registry schema and the apm marketplace verbs.

For the operational concerns that surround a marketplace, see:

Terminal window
apm marketplace init # 1. add the block to apm.yml
$EDITOR apm.yml # 2. describe each package
apm marketplace check # 3. validate refs resolve
apm pack # 4. build marketplace artifacts
git add apm.yml .claude-plugin/marketplace.json
git commit -m "Release v1.0.0" && git tag v1.0.0 && git push --tags

A consumer in another repo then runs:

Terminal window
apm marketplace add acme-org/my-marketplace
apm install example-package@my-marketplace

That is the loop. The rest of this page covers the registry schema and the apm marketplace verbs.

APM uses a single source-of-truth model:

  • apm.yml — hand-edited. The marketplace: block declares your registry: owner, packages, version ranges.
  • .claude-plugin/marketplace.json — generated by apm pack by default. Byte-compatible with Anthropic’s marketplace.json so Claude Code, Copilot CLI, and APM all read the same artefact.
  • .agents/plugins/marketplace.json — optional Codex repo marketplace output. Enable it by adding codex to marketplace.outputs.

Commit every generated file matching your enabled marketplace.outputs. The legacy standalone marketplace.yml is deprecated; if you still have one, run apm marketplace migrate.

Scaffold the block:

Terminal window
apm marketplace init --owner acme-org

This appends a richly commented marketplace: block to apm.yml (creating apm.yml if absent). The minimal shape:

name: my-project
version: 1.0.0
description: Curated plugins for the acme-org engineering team
marketplace:
owner:
name: acme-org
url: https://github.com/acme-org
outputs: # map form (recommended)
claude: {} # default; add codex for Codex output
claude:
output: .claude-plugin/marketplace.json
codex:
output: .agents/plugins/marketplace.json
build:
tagPattern: "v{version}"
packages:
- name: example-package
description: Human-readable description consumers see
source: acme-org/example-package
version: "^1.0.0"
- name: pinned-package
source: acme-org/pinned-package
ref: 3f2a9b1c
- name: local-tool
source: ./packages/local-tool
version: 0.1.0
category: Productivity # required when outputs includes codex

The key in apm.yml is packages:. It becomes plugins: in the compiled marketplace.json — that rename is the only structural transform apm pack performs. Strict schema: unknown keys raise an error, never silently ignored.

Add and edit packages without leaving the shell:

Terminal window
apm marketplace package add acme-org/another-pkg --version "^2.0.0"
apm marketplace package set example-package --version "^1.2.0"
apm marketplace package remove pinned-package

Marketplace output targets use a map-form pattern. The legacy list form (outputs: [claude, codex]) still parses with a deprecation warning. When codex is selected, every package must define category. Codex output maps local entries to source: local, remote entries to source: url, and remote subdirectory entries to source: git-subdir.

Terminal window
apm pack

apm pack resolves every remote packages: entry against git ls-remote, leaves local-path entries untouched, and writes each selected marketplace output atomically. Useful flags:

Terminal window
apm pack --dry-run # resolve and print; do not write
apm pack --offline # cached refs only
apm pack --include-prerelease # allow pre-release tags
apm pack -v # per-entry resolution detail
apm pack --marketplace=claude --json # JSON output for CI pipelines

For the release-gate flags (--check-versions, --check-clean), see Releasing from any CI.

The same apm pack run also produces a bundle to ./build/<name>/ when apm.yml declares dependencies:. Marketplace projects with no dependencies: block produce only marketplace.json. See Pack a bundle for the bundle side.

Terminal window
apm marketplace check # every package's ref/range resolves
apm marketplace doctor # local environment diagnostics
apm marketplace outdated # packages with newer matching tags

check is the gate to run in CI: a missing tag or unresolvable range exits non-zero before you push the release commit.

apm marketplace publish is the optional fan-out: it opens PRs against a list of consumer repos that pin the previous marketplace version, bumping each one to the version you just released.

consumer-targets.yml
targets:
- repo: acme-org/service-a
branch: main
- repo: acme-org/service-b
branch: develop
path_in_repo: apm.yml # default
Terminal window
apm marketplace publish --dry-run # preview
apm marketplace publish --yes # push branches and open PRs
apm marketplace publish --no-pr # push branches, skip gh PR creation

It clones each target, edits its apm.yml to point at the new marketplace ref, pushes a feature branch, and opens a PR via gh. State is journaled to .apm/publish-state.json. Failures in one target do not abort the others; the exit code is non-zero if any target failed.

This flow assumes gh is authenticated and the runner has push access to every target — it is targeted at internal/org marketplaces where you control both sides. Public marketplaces should rely on consumers running apm install --update on their own cadence.

  • packages: not plugins: in the apm.yml source. The plugins: name only appears in the compiled JSON.

  • Both apm.yml (marketplace: block) and marketplace.yml present is a hard error. Pick one; prefer the block and run apm marketplace migrate to consolidate.

  • *.json in .gitignore will silently skip generated files. apm marketplace init warns on this; if you hit it, add an unignore for every enabled output, such as !.claude-plugin/marketplace.json and !.agents/plugins/marketplace.json.

  • Local-path entries skip git resolution. They emit the path verbatim; consumers see the same path. Use metadata.pluginRoot if your plugins live under a common subdirectory.

  • No versions[] array. Each compiled package carries one resolved ref — the highest tag matching the range at build time. Re-run apm pack and re-tag to publish a new version.

  • Bare cross-repo repo: on enterprise (*.ghe.com) marketplaces is refused at install time. Dict-form plugin sources (the source: mapping with nested type: and repo: keys) that point to a different repo than the marketplace project must host-qualify the repo: field. A bare owner/repo cannot be disambiguated from a dependency-confusion attempt where an attacker pre-stages the namespace on public github.com, so the install command fail-closes before validating.

    # Refused -- ambiguous bare form on enterprise marketplace:
    source:
    type: git
    repo: owner/repo
    # Accepted -- enterprise dep on the same host:
    source:
    type: git
    repo: corp.ghe.com/owner/repo
    # Accepted -- declared cross-host dep on public github.com:
    source:
    type: git
    repo: github.com/owner/repo

Org policy can restrict which marketplaces a consumer is allowed to register and which packages it can install from them. That gate runs on the consumer side at install time. See Governance overview for the producer-side implications (signing, allow-listed sources).