Complete guide to APM package dependency management - share and reuse context collections across projects for consistent, scalable AI-native development.
APM dependencies are git repositories containing .apm/ directories with context collections (instructions, chatmodes, contexts) and agent workflows (prompts). They enable teams to:
Share proven workflows across projects and team members
Standardize compliance and design patterns organization-wide
Build on tested context instead of starting from scratch
Maintain consistency across multiple repositories and teams
APM supports any git-accessible host — GitHub, GitLab, Bitbucket, Gitea, Gogs, self-hosted instances, and more. See GitHub Authentication Setup below for how tokens flow to non-GitHub hosts via the git credential helper.
dev.azure.com/org/project/_git/repo or dev.azure.com/org/My%20Project/_git/My%20Repo
Virtual Subdirectory Packages are skill folders from monorepos - they download an entire folder and may contain a SKILL.md plus resources.
Virtual File Packages download a single file (like a prompt or instruction) and integrate it directly.
Marketplaces: Plugins installed as apm install name@marketplace resolve from a registered index. On GitLab-class hosts, monorepo plugins whose sources live in a subdirectory of the marketplace repository itself are supported without hand-writing object-form git: + path: entries. See the Marketplaces guide.
For self-hosted Gitea and Gogs, virtual subdirectory and file packages resolve via the /{owner}/{repo}/raw/{ref}/{path} URL first, then fall back to the Contents API (v1 native, v3 Gogs-compat). On GitLab-class hosts (gitlab.com and self-managed GitLab), virtual subdirectory and file packages resolve via the GitLab REST v4 /projects/{id}/repository/files/{path}/raw endpoint with PRIVATE-TOKEN auth.
Claude Skills are packages with a SKILL.md file that describe capabilities for AI agents. APM can install them and transform them for your target platform:
Fields: git (required), path, ref, alias (all optional). The git value is any HTTPS, HTTP or SSH clone URL.
Explicit URL schemes are honored exactly — see Transport selection for the full contract. Custom ports are preserved across every attempt (including any cross-protocol fallback enabled with --allow-protocol-fallback), so ssh://host:7999/... retried over HTTPS becomes https://host:7999/....
Nested groups (GitLab, Gitea, etc.): APM treats path segments after the host as the repository namespace and name. Shorthand works for many GitLab URLs (for example gitlab.com/group/subgroup/repo). When the namespace is deeply nested or a segment could be read either as part of the repo path or as a virtual path, prefer the object form with an explicit git: URL and path: so install and API resolution stay unambiguous:
dependencies:
apm:
- git: https://gitlab.com/group/subgroup/repo.git
path: registry/pkg
Virtual paths on simple two-segment repos still work in shorthand (gitlab.com/owner/repo/file.prompt.md). For nested-group repos plus a virtual path in the same string, the shorthand is ambiguous — use git: + path::
gitlab.com/group/subgroup/repo/file.prompt.md
# DON'T — ambiguous: APM can't tell where the repo path ends
# → parsed as repo=group/subgroup, virtual=repo/file.prompt.md (wrong!)
When an APM package lives inside a monorepo and depends on a sibling package in the same repository at the same ref, declare the dependency with the literal sentinel git: parent and a path: to the sibling. APM expands parent at resolve time to the consumer’s clone coordinates — you do not have to repeat the host, repo, or ref.
# In agents/pkg-a/apm.yml inside org/monorepo
dependencies:
apm:
- git: parent
path: skills/shared
When org/monorepo is installed at ref main, APM resolves the sibling to the same host, repo_url, and ref, with virtual_path: skills/shared. The lockfile records the expanded coordinates — there is no parent sentinel persisted as durable identity:
# apm.lock.yaml (excerpt)
host: github.com
repo_url: org/monorepo
virtual_path: skills/shared
resolved_ref: main
resolved_commit: <sha>
is_virtual: true
The expansion result is byte-for-byte identical to writing the explicit form below, so swapping between the two never invalidates the lockfile or causes a re-download:
# Equivalent explicit form (verbose, but works outside the monorepo too)
- git: https://github.com/org/monorepo.git
path: skills/shared
ref: main
Use git: parent only when both the consumer and the sibling live in the same git monorepo. A parent reference at the top level of an apm.yml (not transitively pulled in by a parent install) has no monorepo to inherit from and is rejected at resolve time. The path is required, must not be empty, and is normalised to a single relative path — absolute paths and .. traversal are refused.
APM normalizes every dependency entry on write — no matter how you specify a package, the stored form in apm.yml is always a clean, canonical string. This works like Docker’s default registry convention:
GitHub is the default registry. The github.com host is stripped, leaving just owner/repo.
Non-default hosts (GitLab, Bitbucket, self-hosted) keep their FQDN: gitlab.com/owner/repo.
apm install also deploys the project’s own .apm/ content (instructions, prompts, agents, skills, hooks, commands) to target directories alongside dependency content. Local content takes priority over dependencies on collision. This works even with zero dependencies — just apm.yml and a .apm/ directory is enough. See the CLI reference for details and exceptions.
apm update refreshes the APM packages declared in your apm.yml to their latest matching refs. It resolves the dependency graph against the network, prints a structured plan (added / updated / removed), and prompts before mutating anything.
Terminal window
# Interactive: resolve, show plan, prompt [y/N], then install
apmupdate
# Show the plan and exit -- no on-disk changes
apmupdate--dry-run
# CI-safe: skip the prompt
apmupdate--yes
The plan output uses the standard bracket symbols ([~] updated, [+] added, [-] removed) and includes an inline legend in the footer. When nothing has changed, apm update prints All dependencies already at their latest matching refs. and exits cleanly.
For lockfile-only enforcement in CI, pair apm update (in your branch / PR) with apm install --frozen (in CI):
Terminal window
# In CI -- exit 1 if apm.lock.yaml is missing or out of sync with apm.yml
Some packages are only needed during authoring — test fixtures, linting rules, internal helpers. Install them as dev dependencies so they stay out of distributed bundles:
Terminal window
apminstall--devowner/test-helpers
Or declare them directly:
devDependencies:
apm:
- source: owner/test-helpers
Dev dependencies install to apm_modules/ like production deps but are excluded from apm pack plugin output. See Pack & Distribute for details.
Important: plain apm install (no flag) deploys both dependencies and devDependencies — there is currently no --omit=dev flag. The dev/prod separation kicks in at apm pack (plugin format, the default). Maintainer-only primitives that you author yourself MUST live outside .apm/ to be excluded from plugin bundles, because the local-content scanner operates on .apm/ regardless of the devDep marker. See Dev-only Primitives for the canonical pattern.
Install packages from the local filesystem for fast iteration during development.
Terminal window
# Relative path
apminstall./packages/my-shared-skills
# Absolute path
apminstall/home/user/repos/my-ai-package
Or declare them in apm.yml:
dependencies:
apm:
- ./packages/my-shared-skills# relative to project root
- /home/user/repos/my-ai-package# absolute path
- microsoft/apm-sample-package# remote (can be mixed)
How it works:
Files are copied (not symlinked) to apm_modules/_local/<package-name>/
Local packages are validated the same as remote packages (must have apm.yml or SKILL.md)
apm compile works identically regardless of dependency source
Transitive dependencies are resolved recursively (local packages can depend on remote packages)
Anchor rule: a local_path declared inside another local package is resolved relative to that package’s own directory, not the consumer’s project root. This matches npm/pip/cargo workspace behaviour and is what makes mono-repos with sibling helper packages portable across consumers. Sibling layouts that resolve outside the consuming project root (e.g. ../sibling-pkg from a local dep at the project edge) are supported — the consuming developer authored the manifest chain and trusts the layout. The security boundary lives upstream: see the next bullet.
# apm.yml at /repo/apm.yml
dependencies:
apm:
- ./packages/specialized
# apm.yml at /repo/packages/specialized/apm.yml
dependencies:
apm:
- ../base# resolves to /repo/packages/base, NOT /repo/base
Remote packages may not declare local dependencies. A package fetched from owner/repo cannot depend on a local_path — such an entry would reach into the consumer’s filesystem in unpredictable ways. Both relative and absolute local paths are rejected at ERROR severity. Authors of remote packages must publish their dependencies (or vendor them via subdirectory packages).
Re-install behavior: Local deps are always re-copied on apm install since there is no commit SHA to cache against. This ensures you always get the latest local changes.
Lockfile representation: Local dependencies are tracked with source: local and local_path fields. No resolved_commit is stored.
Pack guard:apm pack rejects packages with local path dependencies — replace them with remote references before distributing.
User-scope guard: Local path dependencies are not supported with --global (-g). Relative paths resolve against cwd, which is meaningless at user scope where packages deploy to ~/.apm/. Use remote references (owner/repo) for global installs.
By default, apm install targets the current project — manifest, modules, and lockfile live in
the working directory and deployed primitives go to .github/, .claude/, .cursor/, .opencode/.
Pass --global (or -g) to install to your home directory instead, making packages available
across every project on the machine:
Target detection mirrors project scope: APM auto-detects by ~/.<target>/ directory presence,
falling back to Copilot. Security scanning runs for global installs.
For Claude Code, if CLAUDE_CONFIG_DIR is set (and points inside $HOME), apm install -g --target claude deploys there instead of ~/.claude/ so primitives land where Claude Code reads them.
For private or corporate MCP servers not published to any registry:
mcp:
- name: internal-knowledge-base
registry: false
transport: http
url: "https://mcp.internal.example.com"
env:
API_TOKEN: "${API_TOKEN}"
headers:
Authorization: "Bearer ${API_TOKEN}"
Stdio example:
mcp:
- name: local-db-tool
registry: false
transport: stdio
command: my-mcp-server
args:
- "--port"
- "8080"
Required fields when registry: false:
transport — always required
url — required for http, sse, streamable-http transports
command — required for stdio transport
⚠️ Transitive trust rule: Self-defined servers from direct dependencies (depth=1 in the lockfile) are auto-trusted. Self-defined servers from transitive dependencies (depth > 1) are skipped with a warning by default. You can either re-declare them in your own apm.yml, or use --trust-transitive-mcp to trust all self-defined servers from upstream packages:
env, headers, and args values may reference environment variables using either of two equivalent forms:
Syntax
Meaning
${VAR}
Reference to an environment variable named VAR
${env:VAR}
Same as above (VS Code-style prefix, normalized internally)
How APM materializes a placeholder depends on the target harness:
Copilot CLI (~/.copilot/mcp-config.json): the placeholder is preserved as ${VAR} in the generated config and resolved by Copilot CLI from the host environment at server-start. APM never reads the value, so secrets stay in your shell. Make sure the variable is exported before launching gh copilot.
VS Code (.vscode/mcp.json): the placeholder is rewritten to VS Code’s ${env:VAR} form and resolved by VS Code at server-start.
Other harnesses (Cursor, Windsurf, OpenCode, Claude Desktop, Gemini, Codex): the placeholder is resolved from the current process environment at install time and the literal value is written into the harness config.
The legacy <VAR> syntax is still accepted for backward compatibility but emits a deprecation warning; migrate to ${VAR} in apm.yml.
Run apm install --dry-run to preview MCP dependency configuration without writing any files. Self-defined deps are validated for required fields and transport values; overlay deps are loaded as-is and unknown fields are ignored.
exportAPM_ALLOW_PROTOCOL_FALLBACK=1# CI / migration window
When fallback runs, each cross-protocol retry emits a [!] warning naming
both protocols. Use this to unblock a pipeline while you fix the root
cause — not as a long-term setting.
For SSH key selection (ssh-agent, ~/.ssh/config) and HTTPS token
resolution, see
Authentication.
For the CLI flag and env var reference, see
apm install.
For non-GitHub repositories, APM delegates authentication to git — it never sends GitHub tokens to non-GitHub hosts:
Public repos: Work without authentication via HTTPS
Private repos via SSH: Configure SSH keys for your host. Use an ssh:// or git@host: URL, or set up git config url.<base>.insteadOf to rewrite shorthand to SSH (see Transport selection)
Private repos via HTTPS: Configure a git credential helper — APM allows credential helpers for non-GitHub hosts
This example shows how APM dependencies enable powerful layered functionality by combining multiple specialized packages. The company website project uses microsoft/apm-sample-package as a full APM package and individual prompts from github/awesome-copilot to supercharge development workflows:
company-website/apm.yml
name: company-website
version: 1.0.0
description: Corporate website with design standards and code review
APM automatically retries failed HTTP requests with exponential backoff and jitter. Rate-limited responses (HTTP 429/503) are handled transparently, respecting Retry-After headers when provided. This ensures reliable installs even under heavy API usage or transient network issues.
APM downloads packages in parallel using a thread pool, significantly reducing wall-clock time for large dependency trees. The concurrency level defaults to 4 and is configurable via --parallel-downloads (set to 0 to disable). For sibling subdirectory packages from the same monorepo and ref (e.g. two skills under skills/ in github/awesome-copilot), APM clones the repo bare exactly once into a shared cache and materializes each consumer’s working tree from that cache via git clone --local --shared --no-checkout. This eliminates redundant network fetches and prevents the parallel races that affected earlier sparse-checkout based fetches. When a transitive dependency pins a commit SHA that differs from the ref used for the initial clone, APM fetches that specific commit into the existing bare clone on demand rather than re-cloning.
APM uses instruction-level merging rather than file-level precedence. When local and dependency files contribute instructions with overlapping applyTo patterns:
my-project/
├── .apm/
│ └── instructions/
│ └── security.instructions.md # Local instructions (applyTo: "**/*.py")
│ │ └── apm-sample-package/ # From microsoft/apm-sample-package
│ │ ├── .apm/
│ │ │ ├── instructions/
│ │ │ │ └── design-standards.instructions.md
│ │ │ ├── prompts/
│ │ │ │ ├── design-review.prompt.md
│ │ │ │ └── accessibility-audit.prompt.md
│ │ │ ├── agents/
│ │ │ │ └── design-reviewer.agent.md
│ │ │ └── skills/
│ │ │ └── style-checker/SKILL.md
│ │ └── apm.yml
│ └── github/
│ └── awesome-copilot/ # Virtual subdirectory from github/awesome-copilot
│ └── skills/
│ └── review-and-refactor/
│ ├── SKILL.md
│ └── apm.yml
├── .apm/ # Local context (highest priority)
├── apm.yml # Project configuration
└── .gitignore # Manually add apm_modules/ to ignore
Note: Full APM packages store primitives under .apm/ subdirectories. Virtual file packages extract individual files from monorepos like github/awesome-copilot.
The deployed_files field tracks exactly which files APM placed in your project. This enables safe cleanup on apm uninstall and apm prune — only tracked files are removed.
The mcp_servers field records the MCP dependency references (e.g. io.github.github/github-mcp-server) for servers currently managed by APM. It is used to detect and clean up stale servers when dependencies change.
First install: APM resolves dependencies, downloads packages, and writes apm.lock.yaml
Subsequent installs: APM reads apm.lock.yaml and uses locked commits for exact reproducibility. If the local checkout already matches the locked commit SHA, the download is skipped entirely.
Updating: Use --update to re-resolve dependencies and generate a fresh lockfile. This re-resolves all dependencies, including transitive ones, so stale locked SHAs are never reused.
APM fully resolves transitive dependencies. If package A depends on B, and B depends on C:
apm install contoso/package-a
Result:
Downloads A, B, and C
Records all three in apm.lock.yaml with depth information
depth: 1 = direct dependency
depth: 2+ = transitive dependency
Uninstalling a package also removes its orphaned transitive dependencies (npm-style pruning).
You can use any input form — APM resolves it to the canonical identity stored in apm.yml:
Terminal window
apmuninstallacme/package-a
apmuninstallhttps://github.com/acme/package-a.git# same effect
apmuninstallgit@github.com:acme/package-a.git# same effect
# Also removes B and C if no other package depends on them
Problem: Local files collide with package files during apm installResolution: APM skips files that exist locally and aren’t managed by APM. The diagnostic summary at the end of install shows how many files were skipped. Use --verbose to see which files, or --force to overwrite.