Chapter 8

Security by Default

Understand and rely on APM's install-time security checks without confusing them with runtime sandboxing.

Objective

After this chapter you can use APM's pre-deployment scan, integrity checks, and scoped consent controls while distinguishing their tested coverage from publisher authentication and runtime sandboxing.

Version scope: book 1.2, apm v0.31.0, against the previous reviewed baseline of v0.23.1. Current results come from the operations probes and core MCP probes. Command excerpts below use apm as shorthand for the exact-version executable used in those isolated tests, not a different release selected from your PATH.

Concept/Theory

A prompt is a program, so installation is the gate

Chapter 7 introduced apm audit as the on-demand integrity tool — hidden-Unicode plus drift, explicitly not a CVE feed. The hidden-character scanner also runs during installation: it checks the authorized deployable source set before agent-readable deployment. Acquisition and cache writes can already have happened. The distinction matters: the preventive boundary is activation, not the first disk write, and a later audit answers a different question about the resulting state.

Start from a claim APM makes about the thing it manages: “Agent context is executable — a prompt is a program for an LLM. APM treats it that way” (the three promises). An instruction file steers every generation; a prompt is a runnable procedure; a skill tells the agent how; and an MCP server grants it tool and data access. Installing an agent package therefore imports behavior the same way importing an npm package imports code — which makes both its provenance (where it came from) and its content (what the bytes actually say) security-relevant, not stylistic. This is the intuition Chapter 1 named: “a prompt is, in effect, a program for an LLM, so an unvetted prompt or transitive MCP server is real supply-chain surface.”

Now the timing. You might expect to install a dependency, inspect it, and only then run it. Installation scripts already complicate that expectation in conventional package managers. Agent context adds another problem: a watching harness may ingest a prompt as soon as it appears in .github/prompts/ or another native context directory. There need not be a separate “start” command. A post-install review cannot undo a read that already happened. That is why the v0.31.0 security model places scanning before deployment.

That single idea justifies the whole chapter. An unvetted prompt is unreviewed behavioral input to an agent; an undeclared transitive MCP server is an unaudited capability grant — network, filesystem, tool calls — that arrived as a side effect of installing something else. Consent therefore needs a scope: which dependency, which capability, and which destination? A clean scan or a familiar display name cannot answer all three.

The four properties still fit together. Chapters 4 and 5 established Portability; Chapters 6 and 7 established Reproducibility. Here, those declarations and recorded hashes also support Provenance / security. Chapter 9 adds organizational Governance at the same installation boundary.

In APM

Four controls, different questions

Each control implements part of the pre-deployment boundary. They are not interchangeable: a content scan, an integrity comparison, a graph-depth decision, and an executable approval can legitimately report different things.

APM v0.31.0 controls and their boundaries; the examples below separate documented intent from observed output.
Control Concept Default What to inspect
Hidden-Unicode scan Check content before activation Automatic on the authorized deployable source set Critical findings block affected deployment; Warnings do not
Recorded hashes and audit Detect change relative to a trusted record Install records integrity data; audit checks resulting state Source-tree hashes, deployed-text hashes, and ownership have different scopes
MCP depth-based trust Distinguish chosen from inherited capabilities Always-on depth rule The tested depth-two self-defined candidate was withheld; install still exited 0
Executable Trust Gate Require scoped approval for code-bearing primitives Broad gate is opt-in; plugin bin/ has additional consent rules Compare the decision with actual deployment; the local MCP and parked-hook limits matter

Scan: hidden Unicode can hide instructions in plain sight

Text can contain characters that a reviewer cannot readily see. APM detects known hidden-character classes; it does not decide whether the surrounding instruction is benign (audit reference). The safe controls below contain only benign character-class markers, not an injection payload.

Severity, bare-audit exit codes, and deployment consequences. Observed on apm v0.31.0 in OP08.1.
Severity Illustrative control Bare apm audit exit Install consequence
Critical Bidirectional override U+202E 1 Blocks affected deployment; the mixed-package probe exited 1
Warning Zero-width space U+200B 2 (only if no Critical) Reported, but deploys; the Warning-only install exited 0
Info Legitimate emoji sequence 0 (shown with --verbose) Non-blocking

Use apm audit --file for a standalone rules file or review document. This isolates the character scan from project drift. The security fixture creates critical.md with a benign U+202E marker and warning.md with U+200B. No invisible characters are embedded in the chapter's code.

Standalone scan and strip controls from the security suite. Comments record observed exits, not a literal stdout transcript. Local, inert test text. apm v0.31.0
apm audit --file critical.md                   # observed exit 1
apm audit --file warning.md                    # observed exit 2
apm audit --file critical.md --strip --dry-run  # observed exit 0; file unchanged
apm audit --file critical.md --strip            # observed exit 0; marker removed
apm audit --file critical.md                    # observed exit 0; clean rescan

Warning-only is not automatically a CI failure. In a separate scratch copy of operations/repro, the security suite adds a benign Warning marker to the source instruction and generates a fresh lock through installation. The deployed content is flagged but correctly hashed. Its bare audit exits 2; its CI audit exits 0. Editing an already-deployed file instead would also introduce hash drift — a different test. Here -f json selects machine-readable results, and --no-fail-fast collects diagnostic coverage rather than stopping at the first failed check; neither disables a check.

Installed Warning-only content with matching integrity records; security suite, warning-installed-bare/warning-installed-ci. Observed on apm v0.31.0.
apm install                              # observed exit 0; Warning content deployed
apm audit -f json                        # observed exit 2
apm audit --ci --no-fail-fast -f json      # observed exit 0

Deployment authorization also determines scan scope. A cache is not a directory of universally approved content. Package declarations and selected primitives determine what may enter a harness; the scanner checks that deployable source set.

Three inert OP08.1 controls that locate the scan boundary, observed on apm v0.31.0.
Controlled inputObserved result
Critical-class marker in an ordinary README outside the deployable primitives Clean primitive installed; exit 0
Declared skill subset selects a clean skill, excluding a marker-bearing skill Only the selected skill deployed; exit 0
One clean package and one with a Critical-bearing primitive Clean package deployed, affected primitive withheld; overall exit 1

The last row is not an atomic all-or-nothing batch. Nor is a blocked deployment proof that no cache or staging files were written. --force can bypass the Critical-finding block (install reference); it is an explicit break-glass exception, not normal remediation or a reason to run unsafe content.

For remediation, --strip --dry-run previews the selected file changes without writing them. --file plus --strip rewrites the named file. Project-mode stripping cleans deployed copies, not the package source: the probes observed the marker returning on frozen restore. A durable fix changes the reviewed source or selects a clean upstream revision.

Pin: reproducibility records also detect tampering

Chapter 6's lockfile gives an integrity check a reference point. Keep two hash contracts distinct: a package-tree content_hash covers sorted relative paths and actual materialized bytes; deployed UTF-8 text hashes normalize CRLF to LF. That normalization does not rewrite every source byte, and different harness representations need not have identical hashes (lockfile specification). The CRLF-only control passed CI with unchanged lock bytes; an ordinary text edit did not.

The local operations/repro fixture has one dependency and Copilot, Claude, and Cursor instruction projections. Its runner appends only Benign fixture edit. to a scratch deployed instruction. This tests changed bytes without introducing a malicious instruction:

After the inert edit to .github/instructions/library.instructions.md: ordinary drift is advisory in bare audit and a failure in CI. The repro suite also checks Claude and Cursor. Observed on apm v0.31.0.
apm audit -f json                        # observed exit 0; ordinary drift reported
apm audit --ci --no-fail-fast -f json      # observed exit 1; integrity/drift failure

A hash mismatch establishes change, not its cause. Review the source and the lock together: someone able to replace both content and its expected hash is not authenticated by that hash. apm install --frozen remains a structural manifest/lock control with the bounded cases discussed in Chapter 6, not a comprehensive native-file integrity audit.

Other audit results are not ordinary drift. Observed on apm v0.31.0 in OP07.2 and OP08.1–2.
Controlled stateBare auditCI audit
Invalid canonical deployment owner or active owner 11
Critical-class marker in an unrecorded governed file, whole-project audit 11
Unrelated user-owned hook added to merged Claude settings Not exercised0
APM-owned hook sidecar edited or missing Not exercised1

Ownership is part of the contract: not every edit to a shared native settings file is APM drift. Also keep audit scope explicit. In an independent v0.31.0 rerun of the Critical control above, apm audit ./package -f json scanned the dependency's three deployed files and excluded the unrelated unrecorded marker, exiting 0. The earlier _local/package selector matched no deployed files; its green exit was not evidence of a scoped scan. A whole-project claim needs a whole-project check. Check names and findings are useful evidence; a fixed “eight checks” total is not an API.

Withhold: distinguish direct from transitive MCP

MCP trust uses dependency-graph depth, not filesystem nesting. A package listed in your manifest is direct, at depth 1. A dependency of that package is transitive, at depth 2. “A dependency declares this server” therefore does not by itself mean “this is a transitive server.” The MCP trust model distinguishes these choices.

Configuration-only self-defined MCP controls, observed on apm v0.31.0; no server was launched.
DeclarationObserved result
Server supplied by a direct package dependency, depth 1 Trusted by the depth rule and configured; install exit 0
Self-defined server supplied at depth 2, not re-declared by the consumer Candidate withheld with re-declaration guidance; ordinary skills installed; exit 0
Consumer explicitly re-declares the named server in dependencies.mcp Named server configured; the transitive-candidate warning still appeared; exit 0

The headline is withholding, not a build break. Re-declaration makes one capability a reviewable manifest decision. The documented --trust-transitive-mcp alternative opts into transitive self-defined servers for that invocation; it is broader and does not add the same explicit manifest entry. Prefer the narrow, reviewed declaration. A warning alone does not prove the server is absent: another authorized declaration may have configured it. These controls do not establish the behavior of every registry-resolved or private-source MCP path.

Inspect the correct destination. Copilot CLI and VS Code MCP are separate targets in this release:

Native paths observed in the inert MCP fixture on apm v0.31.0, OP08.3.
RuntimeProject fileContainer
Copilot CLI.github/mcp.jsonmcpServers
VS Code.vscode/mcp.jsonservers
Claude Code.mcp.jsonmcpServers

The broad Executable Trust Gate is opt-in through an executables: block — even executables: {} — or applicable organization executable policy. Its documented scope is dependency hooks, bin/, self-defined MCP, and canvas extensions. Text primitives are outside this particular approval gate, not free of behavioral risk. Their scanning and review still matter (approval reference).

Use apm approve --list and apm policy explain to inspect deciding layers; use apm approve or apm deny to record a reviewed decision. This asks which executable kinds a dependency may supply, whereas MCP depth asks whether the capability was directly chosen or inherited. Keep the mechanisms separate.

Do not turn the broad gate's opt-in status into “all executables deploy unconditionally.” Plugin bin/ has additional explicit invocation consent. The v0.31.0 project-scope probes observed:

Project-scope bin consent outcomes, OP08.3; observed on apm v0.31.0 without running an executable.
Invocation conditionObserved outcome
Noninteractive defaultExit 0; bin executables skipped as not trusted
Explicit --trust-binExit 0; diagnostic said global scope was needed for Claude Code bin deployment
--no-trust-bin or the tested frozen invocationExit 0; no bin deployment

Actual global/user-scope deployment and its permissions were not exercised. Do not widen scope merely to make a test appear successful. The target contract also says invocation consent cannot override policy denial. Neither a consent flag nor an exit 0 is proof that a binary was installed or is safe to run.

When to use / pitfalls

Getting the boundaries right

Use a check for the question it can actually answer. Rely on the install scan before authorized deployment, audit recorded state after materialization, and review each capability before granting consent. Do not substitute a green command for inspection of the relevant output.

Keep credential authority separate too. In the registry probes, a user/project destination-binding mismatch withheld credentials but could still permit an anonymous request; it did not universally block network access. Authentication over HTTP was refused. These tests used only a public non-credential marker. Do not put secrets in manifests or locks, and do not assume a placeholder in one native MCP file proves no plaintext was materialized elsewhere (OP08.3 and core MCP observations).

When a control reports a limit, keep it visible. Do not disable drift checking, broaden trust, or switch to global installation just to produce a clean exit. Review the dependency, inspect the relevant native files and lock records, and retain harness-level restrictions.

Worked example

Meridian reviews a transitive tool into the open

Back to the beat. Meridian's staff engineer adds checkout-review-pack at v0.3.0. The pack is useful markdown, but it depends on checkout-review-tools, which declares local-fetch. The depth-two control demonstrates the withhold-with-warning boundary: useful package content can install while that self-defined MCP candidate is withheld. needs network SKIPPED-needs-network: the private meridian-finance/checkout-review-pack fetch needs unavailable private repository authorization; it is not an executed production transcript.

Meridian reviews the tool and makes v0.3.1's decision explicit: re-declare the reviewed local-fetch connection in the consumer's dependencies.mcp, rather than blanket-trusting transitive servers. Then inspect the native destinations and lock records. If the review rejects the capability, remove the unwanted dependency instead of granting consent. Either way, Chapter 1's invisible capability has become a decision the team can review.

1. Use a direct local control to test the other gate

The following is deliberately a direct dependency control, not a recreation of Meridian's private depth-two graph. The complete v0.31.0 operations/trust fixture contains two manifests, an ordinary instruction, and a hook whose only command is a harmless echo. Its MCP command is intentionally nonexistent. No harness or MCP server is started.

Consumer apm.yml from the local trust fixture. The empty executable block opts into the broad gate; it does not certify withholding. Observed apm v0.31.0.
name: operations-trust
version: 1.0.0
targets: [copilot, claude]
dependencies:
  apm:
    - path: ./package
includes: auto
executables: {}
The dependency's package/apm.yml, alongside its existing .apm/ fixture files. This self-defined entry configures a connection without executing it. Observed apm v0.31.0.
name: friendly-display-name
version: 1.0.0
dependencies:
  mcp:
    - name: operations-trust-fixture
      registry: false
      transport: stdio
      command: operations-trust-not-a-real-server

2. Compare the decision with the files

In the isolated noninteractive test, installation returned success. Inspection reported default-deny for both executable kinds. That was not the final deployment state:

Decision/output comparison in trust-limit, with the OP08.3 explanation probe. Known local-path limitation, not an audit-clean pre-approval example. apm v0.31.0
apm install                              # observed exit 0
apm approve --list                        # hooks and MCP report default-deny
apm policy explain ./package              # verdict: blocked for both kinds
apm audit --ci --no-fail-fast -f json      # observed exit 1; parked-hook replay failure
  • The instruction deployed; the unapproved hook did not.
  • The inspected .github/mcp.json nevertheless contained the local operations-trust-fixture connection. The lock recorded MCP configuration even while the dependency's exec_status was gated_pending_approval.
  • CI replay incorrectly expected the parked hooks, reporting unintegrated for .claude/apm-hooks.json, .claude/settings.json, and .github/hooks/package-notice.json.

These are two upstream limitations: MCP output disagreed with the executable decision, and parked-hook replay failed. They are not network skips. The separate MCP-only control's clean audit also means that “CI passed” cannot prove this approval boundary was enforced.

3. Record reviewed consent, without disguising the limitation

For this inspected inert fixture only, the approval control records consent and reinstalls. It demonstrates how the approved state is represented; approving an unwanted real dependency merely to silence an audit failure would be the wrong response.

Canonical local-path approval followed by installation and audit; all three returned 0 in the inert trust-limit control. apm v0.31.0
apm approve ./package
apm install
apm audit --ci --no-fail-fast -f json
Resulting apm.yml excerpt: the persisted grant uses the canonical dependency identity, not its friendly display name. Observed apm v0.31.0.
executables:
  allow:
    ./package#1.0.0:
      hooks: true
      mcp: true

Approval inspection now reported project-allow. A separate denial control reported project-deny and reproduced the parked-hook replay failure. Deny ceilings remain part of the intended trust ladder, but a decision label is not a substitute for observing deployment. The clean approved control does not erase the pre-approval defect.

4. Reproduce the boundary without starting a service

Use the complete fixture runner, not an install in this book's root. It requires Python 3.10+, PyYAML 6, and an already prepared absolute APM 0.31.0 executable. It copies fixtures into a new short scratch directory, isolates HOME/config/cache, removes inherited credentials, and records results there. Self-defined MCP conflict detection can still attempt a registry identity lookup; these probes bound it to an unavailable loopback endpoint with short timeouts. They do not demonstrate that MCP configuration is intrinsically network-free.

Reproduction entry point for the recorded trust-limit suite. Run from the book checkout; APM operations run only in scratch. The suite reproduces a known failure, not a successful MCP-withholding guarantee. apm v0.31.0
$Apm = '<absolute path to the prepared APM 0.31.0 executable>'
python .\backend\examples\updates\1.2\operations\verify.py --apm $Apm --suite trust-limit

The same runner provides the security, repro, and hooks controls cited earlier. Those use only inert markers, ordinary text edits, or unexecuted harmless hook definitions. None of this exercise requires a real server, credentials, global installation, or an agent session.

Recap & next

Recap

  • Scan before authorized deployment. Agent-readable file presence can activate context. APM scans the deployable source set, not the whole cache before any write; a mixed package batch can partially deploy and still exit 1.
  • Severity and command mode both matter. Critical findings give bare-audit exit 1; Warning-only gives 2. Correctly hashed installed Warning content passed CI with 0. Stripping deployed copies is not a durable source fix.
  • Hashes detect change within a defined scope. Deployed text normalizes line endings for hashing; raw package-tree hashes have a different contract. Ordinary instruction drift was advisory in bare audit and failed CI; invalid ownership failed both. Neither a hash nor frozen restore proves publisher trust or complete native-config coverage.
  • MCP depth and executable consent are separate. Direct means graph depth 1; the tested depth-two candidate was withheld without failing install. Re-declaration is a narrow, reviewable choice. In the direct local executable-gate control, MCP configured despite default-deny, and parked hooks exposed a replay failure.
  • Consent is not confinement. Canonical approvals, lifecycle-script trust, and plugin-bin invocation consent have distinct scopes. APM is not a runtime sandbox, and the checksum-verified Windows executable inspected here reported NotSigned.

Next

The natural next question is organizational: which sources, scopes, and primitives are allowed here, and how do you enforce that reviewed choice? Chapter 9 — Governance & Policy adds the configurable layer through apm-policy.yml: Governance at install time, with tighten-only inheritance and a warn→block rollout. Policy needs a protected enforcement point and evidence that the intended rule ran; it does not turn these controls into runtime sandboxing.