Skip to content

apm pack

Terminal window
apm pack [OPTIONS]

apm pack produces distributable artifacts from the current APM project. It reads apm.yml to decide what to emit:

  • dependencies: mapping present -> a bundle (directory by default, or archive with --archive; see --archive-format). An explicit empty mapping (dependencies: {}) produces a bundle of the package’s local content; an omitted or null dependencies: value does not.
  • marketplace: block present -> selected marketplace artifacts.
  • target: (or targets:) field containing claude or copilot -> ecosystem-specific plugin.json files.
  • Both blocks present -> bundle plus selected marketplace artifacts in a single run.

The bundle is built from apm.lock.yaml. An enriched copy of the lockfile (per-file SHA-256 in bundle_files, plus pack: metadata) is embedded inside the bundle so apm install <bundle> can verify integrity at install time.

Bundles are target-agnostic. The consumer’s project decides where files land at install time – the bundle carries no harness binding. Flags whose scope does not match the detected outputs are silent no-ops, not errors, so the same apm pack invocation works in CI across projects that produce only a bundle, only a marketplace, or both.

Flag Default Description
--claude-plugin on (no-flag default) Select the Claude Code plugin bundle: plugin.json plus plugin-native subdirs (agents/, skills/, commands/, instructions/, hooks/). This is what apm pack produces with zero flags.
--format plugin|agent-plugin|claude|claude-plugin|apm claude-plugin Bundle format selector. agent-plugin is the sole opt-in for the portable Agent Plugins v1 bundle. plugin is a compatibility alias for the Claude Code plugin bundle, not for agent-plugin. claude and claude-plugin also select the Claude Code plugin bundle (the no-flag default). apm emits the legacy APM bundle layout, kept for tooling that still consumes it (e.g. microsoft/apm-action@v1 restore mode). Passing more than one selector (--claude-plugin, --format) is a usage error.
--archive off Produce a .zip archive instead of a directory (previous default: .tar.gz; use --archive-format tar.gz for legacy CI pipelines). Bundle only.
--archive-format zip|tar.gz zip Archive format when --archive is set. zip is natively extractable on Windows and matches the format expected by Claude Code and plugin hosts. tar.gz is typically smaller for text-heavy bundles and preserves the previous default for pipelines that depend on it.
-o, --output PATH ./build Bundle output directory. Does not affect the marketplace.json path.
--force off Allow overwriting on collision. In plugin bundle format, last writer wins instead of first; for generated plugin.json manifests, overwrites an existing file instead of preserving it.
--dry-run off Print what would be packed without writing anything.
--verbose, -v off Show per-file paths and detailed packer output.
--offline off Marketplace: resolve version ranges from cached refs only; skip git ls-remote.
--include-prerelease off Marketplace: allow pre-release tags to satisfy version ranges.
-m, --marketplace FORMATS all configured Comma-separated list of marketplace formats to build. Sentinels: all (every configured format), none (skip marketplace entirely).
--marketplace-path FORMAT=PATH manifest default Override the output path for a specific format. Repeatable. Example: --marketplace-path codex=./dist/codex.json.
--json off Emit machine-readable JSON to stdout. All logs move to stderr. Includes metadata_enrichment.certifiable and per-package outcomes.
--legacy-skill-paths off Bundle skills under per-client paths (e.g. .cursor/skills/) instead of the converged .agents/skills/. Compatibility flag.
--check-versions off Release gate: verify per-package versions agree with the configured marketplace.versioning.strategy (lockstep, tag_pattern, or per_package). Exits 3 on misalignment. Composes with --check-clean and --dry-run.
--check-clean off Read-only release gate: regenerate every configured marketplace output in memory and diff against the same effective path used by apm pack, including --marketplace-path overrides. Exits 4 for drift or uncertifiable remote Claude metadata. It automatically suppresses normal pack writes.
--strict-metadata off Claude marketplace: fail before writing when remote package metadata cannot be fetched. Use it in publishing CI to require those fetches to succeed. Exits 5 before --check-clean runs when both flags are present.
--target, -t VALUE auto-detect Deprecated. Recorded as informational pack.target metadata only; ignored by apm install. Will be removed in a future release.

The metadata_enrichment object has a closed per-package status vocabulary:

Status Meaning Certifiable
fetched Remote apm.yml supplied metadata. yes
empty Remote apm.yml was reachable but had no description or version. yes
local Metadata came from a local package. yes
explicit Fixed description and version came from the marketplace entry. yes
failed The remote manifest could not be fetched. no
offline Fetching was intentionally skipped by --offline. no
{
"metadata_enrichment": {
"certifiable": false,
"outcomes": [
{
"package": "remote-tool",
"status": "failed",
"cause": "request timed out"
}
]
},
"errors": [
{
"code": "marketplace_metadata_uncertifiable",
"message": "remote metadata unavailable; regeneration is uncertifiable"
}
]
}

Default packing reports an uncertifiable result as a warning. --check-clean uses marketplace_metadata_uncertifiable and exits 4; --strict-metadata uses metadata_incomplete and exits 5 before writing.

GitHub-hosted packages can inherit description and version from their remote apm.yml. For GitLab, Azure DevOps, and other hosts, set fixed description and version fields on the marketplace package entry so strict checks can certify without a GitHub metadata request.

Terminal window
apm pack # Claude plugin bundle (default), ./build/
apm pack --archive # Claude plugin bundle as .zip (default)
apm pack --archive --archive-format tar.gz # legacy CI: produce .tar.gz instead
apm pack --format agent-plugin # explicit Agent Plugin v1 bundle
apm pack --format apm -o ./dist # legacy APM bundle layout
Terminal window
apm pack
apm pack --offline --dry-run
# Build only Claude format, output as JSON for CI:
apm pack --marketplace=claude --json
# Override codex output path:
apm pack --marketplace-path codex=./dist/codex-marketplace.json
# Build all formats, preview paths:
apm pack --marketplace=all --json | jq -r '.marketplace.outputs[].path'
Terminal window
apm pack
apm pack --archive --offline
marketplace:
outputs:
claude: {}
codex:
path: ./build/codex-marketplace.json
Terminal window
apm pack --dry-run
apm pack --archive --dry-run -v

Claude plugin bundle (--claude-plugin, default)

Section titled “Claude plugin bundle (--claude-plugin, default)”

A Claude Code plugin directory under --output. Contains:

  • plugin.json – schema-conformant manifest. Convention-dir keys are stripped because Claude Code auto-discovers them.
  • Plugin-native subdirs populated from local source and installed dependencies: agents/, skills/, commands/, instructions/, hooks/, extensions/ (canvas extensions, when the canvas experimental flag is enabled).
    • When .apm/ exists, local primitives and hooks are sourced from .apm/. Root convention sources are skipped with actionable warnings.
    • Without .apm/, supported plugin-native root directories remain pack sources, including after apm init writes includes: auto.
    • An explicit includes: list is exhaustive. A missing or unpackable listed path stops packing instead of falling back to implicit discovery.
  • Installed dependencies are packed exclusively from lockfile-attested deployed_files; the apm_modules cache is never packed (it has no provenance or integrity guarantee). Each attested file is verified against its deployed_file_hashes SHA-256 before inclusion.
    • If the dependency declares skills:, only the named skills are included; the cache cannot add extras.
    • If a dependency has cached primitives but no deployed_files, apm pack fails and tells you to run apm install.
  • A merged hooks.json from the producer’s own hooks. Dependency hook-configs and MCP-configs are not merged into the bundle; dependencies contribute only their attested deployed_files (hook scripts recorded there still map into hooks/).
  • apm.lock.yaml – enriched copy with pack: metadata and a bundle_files map of per-file SHA-256 digests, used by apm install for install-time integrity verification.
  • devDependencies are excluded.

Agent Plugin bundle (--format agent-plugin)

Section titled “Agent Plugin bundle (--format agent-plugin)”

An explicit opt-in. apm pack --format agent-plugin emits a strict, portable Agent Plugins v1 bundle instead of the Claude-compatible layout above. --format plugin does not select this bundle – it is a compatibility alias for the Claude plugin bundle below.

  • plugin.json – identifies the pinned Agent Plugins v1 schema ($schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json"), synthesised from apm.yml the same way as the Claude plugin manifest.
  • skills/ – standard skill bundles. This is the only primitive directory the Agent Plugin bundle carries.
  • mcp.json – MCP server declarations from apm.yml/apm.lock.yaml (present even when empty).
  • apm.lock.yaml, and README.md/LICENSE/CHANGELOG.md when present at the project root.

The portable schema deliberately scopes to skills/ and MCP config so a bundle can be consumed by any Agent-Plugin-aware host, not just APM. Agents, commands, instructions, extensions, hooks, and LSP configuration stay in the Claude-compatible layout instead. If your source project has any of them, apm pack --format agent-plugin fails before writing anything:

Cannot pack Agent Plugin: non-portable primitives would be discarded (agents, hooks).
Agent Plugins v1 portable components are limited to root plugin.json, skills/, and
root mcp.json. Use 'apm pack --format claude-plugin' to preserve agents, hooks in the
legacy Claude client format.

LSP configuration gets its own guidance in the same error, since neither bundle format carries it: configure LSP servers directly in the target client instead.

The legacy APM layout under --output. Files are copied preserving their install-time directory structure. Installed dependencies are packed exclusively from lockfile-attested deployed_files, and each file is verified against its deployed_file_hashes SHA-256 before it is copied (the same integrity gate the Claude plugin format applies) – a file whose bytes no longer match its recorded hash fails the pack with ... does not match the hash recorded in apm.lock.yaml. Files with no recorded hash (older lockfiles) pack without verification. The bundle’s apm.lock.yaml carries the same pack: metadata and bundle_files digests. The project’s own apm.lock.yaml is never modified.

Example enriched lockfile fragment:

pack:
format: apm
packed_at: '2026-03-09T12:00:00+00:00'
bundle_files:
.github/agents/architect.md: a1b2c3...
lockfile_version: '1'
dependencies:
- repo_url: owner/repo

.claude-plugin/marketplace.json by default, plus any additional artifact selected by marketplace.outputs such as .agents/plugins/marketplace.json for Codex. Each remote plugin’s version range is resolved against git ls-remote; local-path entries pass through verbatim. Files are written atomically, and parent directories are created if absent.

Configure marketplace artifact paths in apm.yml with the marketplace.outputs map, keyed by format. Use --marketplace-path FORMAT=PATH to override per-format output paths at pack time.

Remote Claude entries can inherit description and version from their own apm.yml. If APM cannot fetch that metadata, normal packing writes the artifact with an actionable warning so authors can add those fields to the marketplace entry or retry with network access. Use --strict-metadata in publishing CI to fail before writing with uncertifiable remote metadata. --check-clean also fails with exit 4 rather than certifying a regeneration whose metadata could not be fetched.

Ship one APM package; consumers get a native plugin for their tool of choice. When apm.yml declares a target: (or targets:) field containing claude or copilot, apm pack generates an ecosystem-specific plugin.json so the same source tree drops into a Claude Code plugin directory or a Copilot plugin path with no hand-editing.

Ecosystem Output path
claude .claude-plugin/plugin.json
copilot .github/plugin/plugin.json

This runs for the default Claude plugin build (--claude-plugin, or --format plugin|claude|claude-plugin, since plugin is a compatibility alias for the Claude plugin bundle). Under an explicit --format agent-plugin build, the Claude ecosystem manifest is skipped – if target: names only claude and the project has no other agent sources, apm pack --format agent-plugin fails and tells you to use apm pack --claude-plugin instead.

Add one line to apm.yml and pack:

apm.yml
name: my-plugin
version: 1.0.0
target: claude
Terminal window
apm pack # writes .claude-plugin/plugin.json

Use targets: [claude, copilot] instead to emit both .claude-plugin/plugin.json and .github/plugin/plugin.json from the one source tree in a single apm pack.

target: and targets: are mutually exclusive: declaring both is a build error (exit 1). An empty targets: list or an unrecognised ecosystem token is likewise rejected before any artifact is written.

The manifest is synthesised from apm.yml identity fields (name, version, description, author, license). Per-ecosystem differences:

  • Claude: includes mcpServers sourced from .mcp.json when that file declares servers that survive credential stripping.
  • Copilot: omits mcpServers.

.mcp.json routinely embeds secrets that an MCP host injects at startup, so they are removed before the manifest is written – a committed plugin.json never leaks them. Stripping is recursive and applies at any nesting depth:

  • Credential-bearing keys are dropped: env/environment/headers/authorization blocks, plus any key whose name contains token, secret, password, credential, apikey, or key.
  • Secret-shaped values are redacted even when the key name is innocuous: user:pass@host URL userinfo, inline --token=... flags, space-separated --token value pairs, shell ENV=secret prefixes, Bearer/Basic auth headers, and bare provider tokens (GitHub, OpenAI, Slack, AWS, Google, GitLab, npm, PyPI, HuggingFace, Stripe, SendGrid, Supabase, Databricks, and other recognised provider token prefixes) passed as positional args.

A warning lists everything dropped or redacted, led by the consequence (secrets withheld from commit).

If a plugin.json already exists at the target path it is preserved: apm pack warns and skips the write. Re-run with --force to overwrite it (the same flag that governs bundle collisions). The --dry-run flag prevents any writes – the manifest content is computed but not persisted.

Plugin manifest generation runs after BUNDLE and MARKETPLACE phases so the generated file is never accidentally included in the bundle export.

  • Lockfile-attested dependencies. Dependency content is packed exclusively from lockfile deployed_files and verified against deployed_file_hashes; the apm_modules cache is never packed. If a dependency has cached primitives but no deployed_files, apm pack errors and tells you to run apm install.
  • Hidden-character scan. Source files are scanned before bundling. Findings are reported as warnings only – packing is non-blocking. Consumers are protected at install time, where critical findings block.
  • Empty bundle warning. If no package files match after dependency resolution, apm pack emits a warning and exits 0 with an empty bundle. Missing dependency content is an error, not an empty bundle.
  • Share line. On success, apm pack prints Share with: apm install <bundle-path> so the produced bundle is immediately copy-pasteable.
  • Marketplace fallback. With no marketplace: block in apm.yml, a legacy marketplace.yml file is read with a deprecation warning. Both files present is a hard error.
  • Marketplace outputs. Configure via marketplace.outputs map (keyed by format). Claude is included by default. The legacy list form (outputs: [claude]) still parses with a deprecation warning. Use --marketplace= to filter which formats are built in a given invocation.
  • JSON mode. --json makes apm pack machine-friendly: stdout is a single JSON object, all human-readable logs move to stderr. Combine with --marketplace= for selective CI matrix builds.
Code Meaning
0 Success. Requested artifacts written, planned with --dry-run, or validated without writes by --check-clean.
1 Build or runtime error: network failure, ref not found, no tag matches a marketplace range, lockfile read error, or unhandled packer exception.
2 apm.yml schema validation error.
3 --check-versions failed: per-package versions disagree with the configured marketplace versioning strategy.
4 --check-clean failed: marketplace working tree is dirty (regenerated output differs from on-disk file), or remote Claude metadata could not be fetched to certify the comparison.
5 --strict-metadata failed: remote marketplace metadata was unavailable, so APM did not write the artifact.