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
# 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 codex

The 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: 3f2a9b1c

Azure 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: main

After:

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: main

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. Claude output also emits category on any package where it is set, even though only codex requires it.

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

  • 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 deep-dive for the producer-side implications (signing, allow-listed sources).