Skip to content

Drift Detection

Drift is what you find between two apm install runs that should have been identical. This page is the operator’s checklist: the categories APM detects today, the command surface for each, and patterns for sweeping the signal across an estate of repos.

Four concrete categories. Each has a different signal and a different fix.

Category What it is Detected by
Lockfile drift apm.yml and apm.lock.yaml disagree, or deployed files do not match the lockfile apm audit --ci (8 baseline checks)
Cache drift A cached checkout’s HEAD no longer matches resolved_commit in the lockfile Built into every apm install cache hit
Ref drift (stale floats) A locked branch tip or tag has moved upstream since the last install apm outdated
Security-finding drift A primitive in apm_modules/ now contains hidden Unicode that was clean before apm audit

Marketplace-shadow drift – the same plugin name appearing under more than one registered marketplace – is reported inline by apm install when it happens; see security model.

The lockfile-consistency gate. Runs nine baseline checks in order and exits non-zero on the first failure (or on any failure with --no-fail-fast):

lockfile-exists -> ref-consistency -> deployment-ledger-owners
-> deployed-files-present -> no-orphaned-packages
-> skill-subset-consistency -> config-consistency
-> content-integrity -> includes-consent

After the baseline passes, it replays the install in a scratch directory and diffs against the working tree to surface unintegrated, modified, orphaned, and unrecorded files. Root-local primitives use the same source discovery scope as a normal install; the scratch directory is only the deployment destination. unrecorded means replay produced the same normalized bytes as the project but no exact or directory deployed_files claim covers the path; shared merge-hook targets are exempt from unrecorded. For their drift comparison, only APM-owned hook entries are considered; user-owned hooks do not create drift. The APM-owned sidecar remains byte-for-byte checked, so tampered or missing APM-owned hooks report modified. Pass --no-drift to skip the replay. In bare apm audit, the replay remains cache-only and a fresh checkout without a warm cache yields an informational skip. In apm audit --ci, a cold cache instead triggers the lock-pinned scratch self-hydration owned by install/audit_replay.py; if it cannot be materialized, audit fails closed. With --policy <source> it also evaluates the discovered policy against the lockfile. Source: src/apm_cli/commands/audit.py, src/apm_cli/policy/ci_checks.py.

Scans every deployed primitive for hidden Unicode (zero-width characters, bidi overrides, tag characters) and runs the drift replay as advisory output. Exits 0 even when findings exist. Add --strip to remove hidden characters in place; add --file <path> to scan a single file outside the lockfile.

Compares each locked dependency against its remote tip. Tag-pinned dependencies use semver comparison; branch-pinned dependencies compare commit SHAs. Marketplace-sourced dependencies are compared against the marketplace entry’s current ref. Source: src/apm_cli/commands/outdated.py.

Terminal window
apm outdated # project lockfile
apm outdated --global # ~/.apm/ user-scope lockfile
apm outdated -j 8 # 8 parallel remote checks

apm outdated is read-only. It reports moved refs and newer registry releases, even for exact pins. Registry rows distinguish Current, constraint-bound Wanted, and published Latest. apm update still respects the declared constraint; select an outside-constraint release explicitly.

Prints the lockfile entry for a single package (resolved_ref, resolved_commit, deployed files). Use it to confirm what a repo actually has after a drift report names a dependency. Note that apm list lists scripts, not packages.

apm install verifies cache integrity on every cache hit before reusing a checkout. It reads .git/HEAD directly (not via git rev-parse, so a poisoned .git/config cannot subvert the check) and compares the resolved 40-character SHA against the resolved_commit recorded in apm.lock.yaml. On mismatch, the cache entry is evicted and a fresh fetch runs. Source: src/apm_cli/cache/integrity.py.

This means a stale CI runner, a teammate who manually git checkout-ed inside apm_modules/, or a bumped pin without a re-install all surface as either eviction-and-refetch (silent self-heal) or a hard failure when the cache cannot be repopulated – never as wrong content under the right name.

Running apm install before apm audit --ci is the correct pattern when the goal is detecting a developer who forgot to run apm install after editing apm.yml. The install step regenerates deployed files so the subsequent audit can compare them against the lockfile.

That sequence has a blind spot: apm install overwrites every managed file with a clean copy before the audit runs. If a deployed file was modified on disk after the last install – for example a hand-edit to .github/instructions/ – the install step restores the original bytes. The content-integrity check then compares the restored file against a matching hash and reports no finding.

For repos that commit their deployed files, use setup-only: true on the action so it only provides the CLI without running apm install, then run the full CI gate:

- uses: microsoft/apm-action@v1
with:
setup-only: true
- run: apm audit --ci

In setup-only CI, apm audit --ci now self-hydrates a lock-pinned scratch install when apm_modules/ is absent, so drift and config-consistency still run without rewriting the checkout. The content-integrity check still verifies SHA-256 hashes of every deployed file against deployed_file_hashes in apm.lock.yaml. Repos that gitignore deployed outputs still need those files present on disk for deployed-files-present, so keep the full-install CI pattern for that case.

See Enforce in CI for the full recipe and a comparison table of the two patterns.

APM runs per repository. There is no built-in fleet console. The operational pattern is to run the same audit in every repo and centralize the output.

Two shapes work today:

Scheduled CI sweep. Add a workflow that runs nightly in every repo and uploads the SARIF or JSON output as an artifact (or to GitHub code scanning). The dashboard is whatever ingests the artifact.

# .github/workflows/apm-drift.yml -- nightly per repo
name: apm drift
on:
schedule: [{ cron: '0 6 * * *' }]
workflow_dispatch:
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: curl -fsSL https://aka.ms/apm/install | bash
- run: apm install
- run: apm audit --ci --format sarif --output apm-audit.sarif
continue-on-error: true
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: apm-audit.sarif }
- run: apm outdated > apm-outdated.txt
if: always()
- uses: actions/upload-artifact@v4
with: { name: apm-drift, path: 'apm-*' }

Central audit harness. Run a single job that iterates over a list of repos (from a GitHub org listing or a static manifest), clones each one, and runs apm audit --ci --format sarif plus apm outdated. Aggregate the outputs into one dashboard.

Either pattern depends only on documented apm flags. APM does not phone home; the harness is yours.

apm audit --ci accepts --format text|json|sarif and --output; non-CI apm audit also accepts markdown.

Format Use
text (default) Terminal review, pull-request logs
json Custom dashboards, ticketing integrations
sarif GitHub code scanning, Defender for Cloud, any SARIF-aware viewer
markdown Pull-request comments, weekly status digests (non-CI apm audit only)

--format json|sarif|markdown cannot be combined with --strip or --dry-run; those modes are interactive by design.

Once drift is detected, remediation routes back to two pages:

  • Enforce in CI – wire apm audit --ci into branch protection so future drift cannot land. The same command that detects drift here is the one that gates merges.
  • Security model – scope tokens, lock the registry proxy, and tighten apm-policy.yml so the drift you cleaned up cannot reappear from a new source.

For the consumer-side view of what these checks protect against on a single workstation, see drift and secure by default.