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:
- Repo shapes — single-plugin, aggregator, and monorepo-hybrid layouts.
- Versioning strategies — lockstep vs tag-pattern vs per-package.
- Releasing from any CI — the release pipeline that ships your tags.
- Installing from marketplaces — what consumers do with what you publish.
End to end
Section titled “End to end”apm marketplace init # 1. add the block to apm.yml$EDITOR apm.yml # 2. describe each packageapm marketplace check # 3. validate refs resolveapm pack # 4. build marketplace artifactsgit add apm.yml .claude-plugin/marketplace.jsongit commit -m "Release v1.0.0" && git tag v1.0.0 && git push --tagsA consumer in another repo then runs:
apm marketplace add acme-org/my-marketplaceapm install example-package@my-marketplaceThat is the loop. The rest of this page covers the registry schema
and the apm marketplace verbs.
What lives where
Section titled “What lives where”APM uses a single source-of-truth model:
apm.yml— hand-edited. Themarketplace:block declares your registry: owner, packages, version ranges..claude-plugin/marketplace.json— generated byapm packby 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 addingcodextomarketplace.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.
Author the registry
Section titled “Author the registry”Scaffold the block:
apm marketplace init --owner acme-orgThis appends a richly commented marketplace: block to apm.yml
(creating apm.yml if absent). The minimal shape:
name: my-projectversion: 1.0.0description: 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
# Optional: package sources can be relative to this git base. sourceBase: https://gitlab.corp.example.com/platform/agent-marketplace
build: tagPattern: "v{version}"
packages: - name: example-package description: Human-readable description consumers see source: example-package # -> .../agent-marketplace/example-package version: "^1.0.0"
- name: pinned-package source: acme-org/pinned-package # -> .../agent-marketplace/acme-org/pinned-package ref: 3f2a9b1c
- name: local-tool source: ./packages/local-tool version: 0.1.0 category: Productivity # required when outputs includes codexThe key in apm.yml is packages:. It becomes plugins: in the
compiled marketplace.json. Alongside that rename, apm pack normalises
the top-level name to kebab-case in the compiled output — lowercase
letters, digits, and hyphens only — so the Copilot App accepts it (see the
schema reference). The
raw name is preserved for internal resolution and, for Codex, as
interface.displayName. When a rewrite occurs, apm pack prints a warning
showing both the original and emitted name. Strict schema: unknown keys
raise an error, never
silently ignored.
Use sourceBase when packages live under the same enterprise git base.
The base may target any supported host — GitHub.com, GitHub Enterprise,
self-hosted GitLab, or Azure DevOps. The host is preserved end to end, so a
consumer installs from the same host you authored on. Any relative source
composes onto the base, including two-segment values like
acme-org/pinned-package. Host-prefixed sources like github.com/acme/tool,
full HTTPS URLs, and local ./ paths remain per-entry overrides. If
sourceBase is absent, existing owner/repo source behavior is unchanged.
See the manifest schema
for the full validation and override rules.
The generated source object is also a producer-to-consumer contract.
apm pack emits source: url for a remote repository and
source: git-subdir when subdir is set.
apm install <package>@<marketplace> accepts both forms, derives the package host from
the generated entry rather than from the marketplace host, and preserves
the generated path and ref.
For an Azure DevOps marketplace, point sourceBase at the
https://dev.azure.com/{org}/{project}/_git base; relative sources compose
onto it and the dev.azure.com host is kept on the consumer side:
marketplace: sourceBase: https://dev.azure.com/contoso/platform/_git packages: - name: agent-skills source: agent-skills # -> .../contoso/platform/_git/agent-skills ref: 3f2a9b1cAzure DevOps authentication uses ADO_APM_PAT (with an az CLI bearer
fallback); see authentication.
Before:
marketplace: packages: - name: review source: https://gitlab.corp.example.com/platform/agent-marketplace/review ref: v1.0.0 - name: pinned source: https://gitlab.corp.example.com/platform/agent-marketplace/acme-org/pinned-package ref: mainAfter:
marketplace: sourceBase: https://gitlab.corp.example.com/platform/agent-marketplace packages: - name: review source: review ref: v1.0.0 - name: pinned source: acme-org/pinned-package ref: mainAdd and edit packages without leaving the shell:
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-packageMarketplace 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. Claude output also emits category on any
package where it is set, even though only codex requires it.
apm packapm pack resolves every remote packages: entry against
git ls-remote, leaves local-path entries untouched, and writes each
selected marketplace output atomically. Useful flags:
apm pack --dry-run # resolve and print; do not writeapm pack --offline # cached refs onlyapm pack --include-prerelease # allow pre-release tagsapm pack -v # per-entry resolution detailapm pack --marketplace=claude --json # JSON output for CI pipelinesFor 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.
Validate before you ship
Section titled “Validate before you ship”apm marketplace check # every package's ref/range resolvesapm doctor # local environment diagnosticsapm marketplace outdated # packages with newer matching tagscheck is the gate to run in CI: a missing tag or unresolvable
range exits non-zero before you push the release commit.
Pitfalls
Section titled “Pitfalls”-
packages:notplugins:in theapm.ymlsource. Theplugins:name only appears in the compiled JSON. -
Both
apm.yml(marketplace:block) andmarketplace.ymlpresent is a hard error. Pick one; prefer the block and runapm marketplace migrateto consolidate. -
*.jsonin.gitignorewill silently skip generated files.apm marketplace initwarns on this; if you hit it, add an unignore for every enabled output, such as!.claude-plugin/marketplace.jsonand!.agents/plugins/marketplace.json. -
Local-path entries skip git resolution. They emit the path verbatim; consumers see the same path. Use
metadata.pluginRootif 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-runapm packand 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 (thesource:mapping with nestedtype:andrepo:keys) that point to a different repo than the marketplace project must host-qualify therepo:field. A bareowner/repocannot be disambiguated from a dependency-confusion attempt where an attacker pre-stages the namespace on publicgithub.com, so the install command fail-closes before validating.# Refused -- ambiguous bare form on enterprise marketplace:source:type: gitrepo: owner/repo# Accepted -- enterprise dep on the same host:source:type: gitrepo: corp.ghe.com/owner/repo# Accepted -- declared cross-host dep on public github.com:source:type: gitrepo: github.com/owner/repo
Governance
Section titled “Governance”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 deep-dive for the producer-side implications (signing, allow-listed sources).
Where next
Section titled “Where next”- Repo shapes — pick a layout for your producer repo.
- Versioning strategies — how
--check-versionsenforces version alignment. - Releasing from any CI — ship your tagged releases from GitHub Actions, GitLab, Jenkins, or Azure DevOps.
- Installing from marketplaces — the consumer flow your marketplace feeds into.