Skip to content

Security Model

This page documents APM’s security posture for enterprise security reviews, compliance audits, and supply chain assessments.

Traditional package managers install code that sits inert until a developer or CI pipeline explicitly executes it. Between npm install and npm start, there is a gap — time for npm audit, code review, and policy checks.

Agent configuration has no such gap. The moment a skill, instruction, or prompt file lands in .github/prompts/ or .claude/agents/, any IDE agent watching the filesystem — Copilot, Cursor, Claude Code — may already be ingesting it. There is no “execution step.” File presence IS execution.

This changes the security model fundamentally. APM treats package deployment as a pre-deployment gate: scan first, deploy only if clean.

APM is a build-time dependency manager for AI agent configuration. It performs four operations:

  1. Resolves git repositories — clones or sparse-checks-out packages from GitHub or Azure DevOps.
  2. Deploys static files — copies markdown, JSON, and YAML files into project directories (.github/, .claude/, .cursor/, .opencode/).
  3. Generates compiled output — produces AGENTS.md, CLAUDE.md, and similar files from templates and prompts.
  4. Records a lock file — writes apm.lock.yaml with exact commit SHAs for every resolved dependency.

APM has no runtime footprint. Once apm install or apm compile completes, the process exits.

  • No runtime component. APM generates files then terminates. It does not run alongside your application.
  • No network calls after install. All network activity (git clone/fetch) occurs during dependency resolution. There are no callbacks, webhooks, or phone-home requests.
  • No arbitrary code execution. APM does not execute scripts from packages, evaluate expressions in templates, or run downloaded code.
  • No access to application data. APM never reads databases, API responses, application state, or user data.
  • No persistent background processes. APM does not install daemons, services, or scheduled tasks.
  • No telemetry or data collection. APM collects no usage data, analytics, or diagnostics. Nothing is transmitted to Microsoft or any third party.

APM resolves dependencies directly from git repositories. There is no intermediary registry, proxy, or mirror.

Every resolved dependency is recorded in apm.lock.yaml with its full commit SHA:

lockfile_version: "1"
dependencies:
- repo_url: owner/repo
host: github.com
resolved_commit: a1b2c3d4e5f6...
resolved_ref: main
depth: 1
deployed_files:
- .github/skills/example/skill.md

The resolved_commit field is a full 40-character SHA, not a branch name or tag. Subsequent apm install calls resolve to the same commit unless the lock file is explicitly updated.

APM does not use a package registry. Dependencies are specified as git repository URLs in apm.yml. This eliminates the registry compromise vector entirely — there is no centralized service that can be poisoned to redirect installs.

Researchers have found hidden Unicode characters embedded in popular shared rules files. Tag characters (U+E0001–E007F) map 1:1 to invisible ASCII. Bidirectional overrides can reorder visible text. Zero-width joiners create invisible gaps. Variation selectors attach to visible characters, embedding invisible payload bytes that AST-based tools cannot detect. The Glassworm campaign (2026) exploited this mechanism to compromise repositories and VS Code extensions. LLMs tokenize all of these individually, meaning models process instructions that developers cannot see on screen.

SeverityCharactersRisk
CriticalTag characters (U+E0001–E007F), bidi overrides (U+202A–E, U+2066–9)Hidden instruction embedding. Zero legitimate use in prompt files.
CriticalVariation selectors 17–256 (U+E0100–E01EF)Glassworm attack vector — invisible payload encoding. Zero legitimate use in prompt files.
WarningZero-width spaces/joiners (U+200B–D), mid-file BOM (U+FEFF)Common copy-paste debris, but can hide content. ZWJ inside emoji sequences is downgraded to info.
WarningVariation selectors 1–15 (U+FE00–FE0E)CJK typography / text presentation selectors. Uncommon in prompt files.
WarningBidi marks (U+200E–F, U+061C)Invisible directional marks. No legitimate use in prompt files.
WarningInvisible operators (U+2061–4)Zero-width math operators. No legitimate use in prompt files.
WarningAnnotation markers (U+FFF9–B)Interlinear annotation delimiters that can hide text.
WarningDeprecated formatting (U+206A–F)Deprecated since Unicode 3.0, invisible.
InfoNon-breaking spaces (U+00A0), unusual whitespace (U+2000–200A)Mostly harmless, flagged for awareness.
InfoEmoji presentation selector (U+FE0F)Common with emoji, informational only.

During apm install, source files in apm_modules/ are scanned before any integrator copies them to target directories:

download → scan source → block or deploy → report
  • Critical findings block deployment. The package is downloaded and cached so you can inspect it (apm_modules/owner/package/), but nothing reaches agent-readable directories.
  • Warnings are non-blocking. Zero-width characters are flagged in the diagnostics summary. Files are deployed normally.
  • --force overrides the block. Consistent with existing collision semantics — an explicit “I know what I’m doing.”
  • Multi-package installs continue. A blocked package doesn’t stop other packages from installing. After all packages are processed, apm install exits with code 1 if any package was blocked — failing the CI step.

Content scanning extends beyond install:

  • apm compile scans compiled output (AGENTS.md, CLAUDE.md, commands) before writing to disk. Critical findings cause apm compile to exit with code 1 after writing — defense-in-depth since source files were already scanned at install, but compilation assembles content from multiple sources.
  • apm pack scans files before bundling. This catches hidden characters before a package is published, preventing authors from accidentally distributing tainted content.
  • apm unpack scans bundle contents before deployment. This is a pre-deployment gate matching apm install — critical findings block deployment unless --force is used.

apm audit scans deployed files or any arbitrary file, independent of the install flow:

Terminal window
apm audit # Scan all installed packages
apm audit --file .cursorrules # Scan any file
apm audit --strip # Remove hidden characters (preserves emoji)
apm audit --strip --dry-run # Preview what --strip would remove

The --file flag is useful for inspecting files obtained outside APM — downloaded rules files, copy-pasted instructions, or files from pull requests.

For CI pipelines, apm audit supports SARIF, JSON, and Markdown output:

Terminal window
apm audit -f sarif -o audit.sarif # GitHub Code Scanning
apm audit -f json -o report.json # Machine-readable
apm audit -f markdown -o report.md # Step summaries

See Content scanning with apm audit for usage details and exit codes.

Content scanning detects hidden Unicode characters. It does not detect:

  • Plain-text prompt injection (visible but malicious instructions)
  • Homoglyph substitution (visually similar characters from different scripts)
  • Semantic manipulation (subtly misleading but syntactically normal text)
  • Binary payload embedding

--strip removes dangerous and suspicious characters (critical and warning) from deployed copies while preserving legitimate content like emoji and whitespace. Zero-width joiners inside emoji sequences (e.g. 👨‍👩‍👧) are recognized and preserved. Use --strip --dry-run to preview what would be removed before modifying files. Strip does not modify the source package — the next apm install restores them. For persistent remediation, fix the upstream package or pin to a clean commit.

  • Hook transparency — display hook script contents during install so developers can review what will execute.

APM computes a SHA-256 hash of each downloaded package’s file tree and stores it in apm.lock.yaml as content_hash. On subsequent installs, cached packages are verified against the lockfile hash. A mismatch triggers a warning and re-download.

apm.lock.yaml
dependencies:
- repo_url: https://github.com/acme-corp/security-baseline
resolved_commit: a1b2c3d4e5f6...
content_hash: "sha256:9f86d081884c7d659a2feaa0c55ad015..."

The hash is deterministic — computed over sorted file paths and contents, independent of filesystem metadata (timestamps, permissions). .git/ and __pycache__/ directories are excluded.

Lock files generated before this feature omit content_hash. APM handles this gracefully — verification is skipped and the hash is populated on the next install.

See the Lock File Specification for field details.

APM deploys files only to controlled subdirectories within the project root.

All deploy paths are validated before any file operation:

  1. No .. segments. Any path containing .. is rejected outright.
  2. Allowed prefixes only. Paths must start with an allowed prefix (.github/, .claude/, .cursor/, or .opencode/).
  3. Resolution containment. The fully resolved path must remain within the project root directory.

A path must pass all three checks. Failure on any check prevents the file from being written.

Symlinks are never followed during artifact operations:

  • Tree copy operations skip symlinks entirely — they are excluded from the copy via an ignore filter.
  • MCP configuration files that are symlinks are rejected with a warning and not parsed.
  • Manifest parsing requires files to pass both .is_file() and not .is_symlink() checks.
  • Archive creationapm pack excludes symlinks from bundled archives. Packaged artifacts contain no symbolic links, preventing symlink-based escape attacks in distributed bundles.

This prevents symlink-based attacks that could escape allowed directories or cause APM to read or write outside the project root.

When APM deploys a file, it checks whether a file already exists at the target path:

  • If the file is tracked in the managed files set (deployed by a previous APM install), it is overwritten.
  • If the file is not tracked (user-authored or created by another tool), APM skips it and prints a warning.
  • The --force flag overrides collision detection, allowing APM to overwrite untracked files.

APM separates production and development dependencies:

  • Production dependencies (dependencies.apm) are included in plugin bundles and shared packages.
  • Development dependencies (devDependencies.apm, installed via apm install --dev) are resolved and cached locally but excluded from apm pack --format plugin output.

This prevents transitive inclusion of development-only packages (test fixtures, linting rules, internal helpers) in distributed artifacts. The lockfile marks dev dependencies with is_dev: true for explicit tracking. See the Lock File Specification for field details.

APM integrates MCP (Model Context Protocol) server configurations from packages. Trust is explicit and scoped by dependency depth.

MCP servers declared by your direct dependencies (packages listed in your apm.yml) are auto-trusted. You explicitly chose to depend on these packages, so their MCP server declarations are accepted.

MCP servers declared by transitive dependencies (dependencies of your dependencies) are blocked by default. Transitive MCP servers can request tool access, file system permissions, or network capabilities — blocking them ensures that adding a prompt package cannot silently grant MCP access to an unknown transitive dependency.

To allow transitive MCP servers, you must either:

  • Re-declare the dependency in your own apm.yml, promoting it to a direct dependency.
  • Pass --trust-transitive-mcp to explicitly opt in to transitive MCP servers for that install.

APM authenticates to git hosts using personal access tokens (PATs) read from environment variables.

PurposeEnvironment variables (checked in order)
GitHub packagesGITHUB_APM_PAT, GITHUB_TOKEN, GH_TOKEN
Azure DevOps packagesADO_APM_PAT
  • Never stored in files. Tokens are read from the environment at runtime. They are never written to apm.yml, apm.lock.yaml, or any generated file.
  • Never logged. Token values are not included in console output, error messages, or debug logs.
  • Scoped to their git host. A GitHub token is only sent to GitHub. An Azure DevOps token is only sent to Azure DevOps. Tokens are never transmitted to any other endpoint.

For GitHub, a fine-grained PAT with read-only Contents permission on the repositories you depend on is sufficient.

VectorTraditional package managerAPM
Registry compromiseAttacker poisons central registryNo registry exists
Version substitutionMalicious version replaces legitimate oneLock file pins exact commit SHA; content hash detects post-download tampering
Post-install scriptsArbitrary code runs after installNo code execution
TyposquattingSimilar package names on registryDependencies are full git URLs
Build-time injectionMalicious build steps executeNo build step — files are copied
Hidden content injectionNot applicable (binary packages)Pre-deploy scan blocks critical hidden Unicode; apm audit for on-demand checks

Not without detection. APM scans all package source files before deployment. Critical hidden characters (tag characters, bidi overrides) block deployment. apm audit provides on-demand scanning for any file, including those obtained outside APM.

The apm.lock.yaml file records every dependency (with exact commit SHA) and every file deployed. It is a plain YAML file suitable for automated policy checks, diff review, and compliance tooling. See Governance & Compliance for audit workflows.

APM is distributed as a PyPI package (apm-cli) and as pre-built binaries attached to GitHub Releases under the microsoft organization. Both distribution channels use GitHub Actions workflows with pinned dependencies and are auditable through the public repository.

APM is open source under the MIT license, hosted on GitHub under the microsoft organization. The full source code, build pipeline, and release process are publicly auditable.