Skip to content

Pack a bundle

A bundle is the artifact you hand to a consumer when you do not want to publish to a registry. It is a directory (or archive — .zip by default, .tar.gz via --archive-format tar.gz) containing a plugin.json, your primitive folders, and an embedded apm.lock.yaml that pins every file by SHA-256. Build it with one command from a project that has .apm/ and apm.yml:

Terminal window
apm pack

This is the producer side of Deploy a local bundle. Consumers who receive the artifact run apm install ./your-bundle and skip the registry resolver entirely.

By default apm pack writes a plugin-format directory under ./build/:

build/<your-package>/
+-- plugin.json
+-- agents/
+-- skills/
+-- commands/
+-- hooks/
+-- apm.lock.yaml # embedded: pins every file by SHA-256

The success line tells you exactly what to share:

$ apm pack
[+] Packed 7 file(s) -> build/my-pkg
[>] Plugin bundle ready -- contains plugin.json plus plugin-native
directories (agents/, skills/, commands/, ...) and an embedded
apm.lock.yaml for install-time integrity verification.
[i] Share with: apm install build/my-pkg

Add --archive to get a single archive (.zip by default; use --archive-format tar.gz for legacy CI pipelines) instead of a directory; use -o to change the output location (default ./build).

Terminal window
apm pack --archive -o ./dist
# -> ./dist/my-pkg-<version>.zip

plugin.json is the bundle’s identity card. Only name is required. APM synthesises one from apm.yml if you do not author it yourself, mapping these fields:

apm.yml fieldplugin.json field
namename (required)
versionversion
descriptiondescription
authorauthor
licenselicense
homepagehomepage
repositoryrepository
keywordskeywords

The author field accepts a plain string ("Jane Doe" maps to {name: "Jane Doe"}) or a structured object ({name, email?, url?} — all keys optional except name):

# String form (backward-compatible):
author: Jane Doe
# Structured form:
author:
name: Jane Doe
email: jane@example.com
url: https://example.com/jane

Author your own plugin.json at the project root (or under .github/plugin/, .claude-plugin/, or .cursor-plugin/) when you need fields APM does not synthesise — otherwise leave it to apm pack and keep apm.yml as the source of truth. See Package anatomy for the full schema.

Integrity: how install verifies the bundle

Section titled “Integrity: how install verifies the bundle”

apm pack writes pack.bundle_files into the embedded apm.lock.yaml — a mapping of every file’s relative path to its SHA-256 digest. On the consumer side, apm install <bundle> rehashes every file and rejects the bundle if:

  • any hash does not match
  • any file listed in pack.bundle_files is missing
  • any file is present in the bundle but not listed in the manifest
  • any path is a symlink

The manifest is the source of truth. Tampering after pack time is detected before any file lands in the project. You do not configure this — it runs on every apm install <bundle>.

Three common ways to hand off a bundle:

  • Directory + git. Commit build/<pkg>/ to a release branch or a separate artifacts repo. Consumers git clone and run apm install ./build/<pkg>.
  • Archive + GitHub release. apm pack --archive then upload the .zip as a release asset. Consumers download and run apm install ./<pkg>-<version>.zip.
  • Marketplace entry. If your project also has a marketplace: block in apm.yml, apm pack builds marketplace.json alongside the bundle. See Publish to a marketplace.

For the consumer flags that apply (--target, --global, --force, --dry-run), see Deploy a local bundle.

apm pack is intentionally liberal: it collects primitives from both .apm/<type>/ subdirectories and from convention directories at the package root (agents/, skills/, instructions/, etc.). This lets you author in whichever layout feels natural during development.

apm install is per-primitive and stricter. Each integrator has its own discovery rules. For some primitive types the root convention directory is not scanned at install time, so a file that appears in the pack bundle may be silently skipped by a downstream apm install call.

The table below shows what apm install actually scans for each primitive type:

Primitiveapm install scansRoot alternative accepted?
instruction.apm/instructions/*.instructions.mdNo
command (prompt).apm/prompts/*.prompt.mdNo
hook.apm/hooks/*.jsonYes: hooks/*.json
agent.apm/agents/**/*.agent.md, .apm/chatmodes/*.chatmode.mdYes: *.agent.md and *.chatmode.md at package root
skill.apm/skills/<name>/SKILL.mdYes: skills/<name>/SKILL.md (SKILL_BUNDLE or MARKETPLACE_PLUGIN)

Source: src/apm_cli/integration/instruction_integrator.py, src/apm_cli/integration/command_integrator.py, src/apm_cli/integration/hook_integrator.py, src/apm_cli/integration/agent_integrator.py, src/apm_cli/integration/skill_integrator.py.

Canonical layout for marketplace publishers

Section titled “Canonical layout for marketplace publishers”

If you publish a plugin that consumers install via apm install, use .apm/<type>/ for every primitive type. This layout is the only one that works symmetrically through both apm pack (export) and apm install (discovery).

plugins/my-plugin/
apm.yml # minimal: name, version, description
.apm/
agents/
security.agent.md
skills/
my-skill/
SKILL.md
instructions/
style.instructions.md # ONLY discovered from .apm/instructions/
prompts/
review.prompt.md # ONLY discovered from .apm/prompts/
hooks/
pre-tool.json

To verify what your bundle actually contains before distributing it, run:

Terminal window
apm pack --dry-run --verbose

The verbose output lists every file and any path remappings. Any instruction or prompt you expect to be included should appear there before you share the bundle.

When one repo ships multiple plugins and a marketplace index, give each plugin its own apm.yml and .apm/<type>/ source tree:

my-publisher-repo/
apm.yml # root: marketplace: block only
plugins/
plugin-a/
apm.yml # per-plugin manifest
.apm/
agents/
expert.agent.md
instructions/
rules.instructions.md
plugin-b/
apm.yml
.apm/
skills/
my-skill/
SKILL.md

Per-plugin apm pack (run from each plugin directory) emits the plugin bundle. The root apm pack builds the marketplace index. See Repo shapes for the full layout options.

Do not use --format apm for bundles you expect consumers to install. The legacy APM bundle layout has no plugin.json and apm install rejects it with a targeted error. The flag exists for tooling that still consumes the older layout; new bundles should use the default --format plugin. If you only have a legacy artifact, repack it:

Terminal window
apm pack --format plugin --archive

Do not set --target. The flag is deprecated. Bundles are target-agnostic: the consumer’s project decides which harness layouts receive files at install time. APM records the value in pack.target as informational metadata only and prints a deprecation warning.

Empty bundle warning. If apm pack reports “No deployed files found”, your apm.lock.yaml has no deployed_files entries. Run apm install first to populate it — apm pack packs the files your last install actually deployed, not the raw .apm/ source tree.

Dry-run before sharing. Use apm pack --dry-run --verbose to see the full file list (and any path remappings) without writing anything.