---
slug: "cve-pre-trust-window"
name: "CVE: The Pre-Trust Execution Window"
packType: "security"
canonicalPattern: "n/a"
version: "0.1.0"
trust: "Community"
publisher: "Agent Workspace"
updatedAt: "2026-04-19"
---

# CVE: The Pre-Trust Execution Window

> 4 CVEs share one root cause: extensions execute before the trust dialog renders.

## Summary

A security pack targeting the pre-trust execution window documented in VILA-Lab's Dive into Claude Code (arXiv 2604.14228). Extensions — plugins, MCP servers, hooks, skill manifests — can execute code during CLI initialization *before* the trust dialog appears, creating a structurally privileged attack window that lives outside the deny-first permission pipeline. The paper's README and Safety section tie four patched CVEs to this root cause. This is NOT prompt injection at runtime — it is code execution at load time, with the user's full OS privileges, before the user has been shown what they are trusting. Pair with `injection-surface-audit` for runtime content attacks; use this pack when you are building or auditing a harness that loads third-party extensions. CC-BY-NC-SA-4.0 source terms apply.

## Install

```sh
npx attrition-sh pack install cve-pre-trust-window
```

### Claude Code / AGENTS.md snippet

```md
Skill `cve-pre-trust-window` is installed at .claude/skills/cve-pre-trust-window/SKILL.md. Invoke on any PR that (a) changes the harness boot sequence, (b) adds a new extension mechanism (plugin, MCP server, hook, skill loader), (c) changes when the trust dialog renders, or (d) parses a third-party manifest during startup. Block release until every item in §Mitigation Checklist is addressed or explicitly waived with staff-engineer sign-off. This pack is a security gate, not a suggestion.
```

## Contract

_No execution contract defined for this pack type._

## Layers

_No three-layer split defined for this pack type._

## Use When

- Building or auditing a harness that loads third-party extensions (plugins, MCP servers, skills, hooks) at boot.
- Implementing or changing the trust dialog or its render order relative to extension initialization.
- Reviewing a CVE advisory that references 'code execution before trust prompt' or 'load-time execution'.
- Onboarding a new plugin format — before the first plugin ships, pin the boot-order invariant.
- Post-incident triage when an extension ran unexpectedly or ran before the user confirmed trust.

## Avoid When

- Your harness has no third-party extension surface — the pack's recommendations add friction without matching risk.
- You need protection against prompt injection in model output or fetched content — use `injection-surface-audit`.
- You are building a web chat UI with no local extension mechanism — there is no pre-trust window to close.
- You want a formal red-team engagement — this is a structured self-audit + mitigation checklist, not a pentest.

## Key Outcomes

- The harness boot sequence has a single, audited boundary: trust dialog renders before any third-party code parses, initializes, or executes.
- Third-party manifests (plugin.json, mcp.json, SKILL.md frontmatter) are parsed in a strict read-only context with no eval / require / subprocess spawn.
- MCP stdio transports do not spawn subprocesses until after trust is confirmed.
- Hooks do not fire on installation-probe runs; a probe is a read-only manifest validation.
- Signed publisher provenance is required for any extension that enters the pre-trust parse path, and the signature is verified before parse.

## Minimal Instructions

## Minimal mitigation — the pre-trust invariant

The rule: **no third-party code executes before the user has been shown what they are trusting and clicked OK.**

Verify these five checks on every harness boot path. If any fail, block the release.

1. **Trust dialog renders FIRST.** The render order is: parse user config → validate harness integrity → render trust dialog → wait for OK → then load any third-party extension. If your cli.js parses plugin init scripts before trust renders, you have the CVE shape.

2. **Manifest parse is read-only.** Plugin / MCP / skill manifests are parsed by a strict JSON / YAML parser with NO `eval`, `require`, `import()`, template-string evaluation, or reference resolution that fetches files. YAML frontmatter MUST NOT support executable expressions (no `!!js` tags, no anchors resolving to code paths).

3. **No subprocess spawn until trust.** MCP stdio transports, hook shell commands, and plugin init scripts MUST defer `spawn()` / `exec()` until after the trust dialog is confirmed. The probe that validates a manifest must not run the manifest's code.

4. **No environment-variable bypass.** The trust dialog cannot be skipped by `CLAUDE_SKIP_TRUST=1` or similar. Env-var shortcuts for trust belong to CI images only, and only when the CI image is in a compile-time allow-list.

5. **Signed publisher provenance required for pre-trust parse.** Only extensions whose publisher signature verifies against a pinned keyring may enter the parse path during boot. Unsigned extensions queue until after trust and load in a restricted context.

If all five pass, the pre-trust window is closed. Run the full checklist (§Mitigation Checklist in fullInstructions) quarterly and post-incident.

## Full Instructions

## Full reference: the pre-trust execution window

### 1. What the paper actually says

From VILA-Lab's Dive into Claude Code (README §Key Highlights, Safety and Permissions §Pre-trust execution window, arXiv 2604.14228):

> **4 CVEs Reveal a Pre-Trust Window** — Extensions execute *before* the trust dialog appears.

And:

> **Pre-trust execution window:** 2 patched CVEs share this root cause — hooks and MCP servers execute during initialization *before* the trust dialog appears, creating a structurally privileged attack window outside the deny-first pipeline.

The key-highlights count (4 CVEs) and the narrow safety-section excerpt (2 patched CVEs sharing this root cause) both point to the same structural shape: *the deny-first permission system guards runtime tool calls, but the code that runs during boot — parsing, initializing, spawning — is outside that pipeline.* Attackers who can get a plugin / MCP server / hook installed get code execution at load time, with the user's OS privileges, before the user has clicked OK on anything.

This is **not prompt injection.** Prompt injection is a runtime attack on model-interpreted text. This is **load-time code execution** — the attacker's code is executed as code, by the CLI's interpreter, during startup.

### 2. Why the pre-trust window exists

Modern agent harnesses load a lot of user-controllable configuration at boot:
- Plugin manifests (commands, agents, skills, hooks, MCP configs).
- MCP server registrations (often with a command to spawn a subprocess).
- Hook scripts wired to 27 lifecycle events.
- Skill manifests with 15+ YAML frontmatter fields.

For the CLI to *render* a meaningful trust dialog, it has to know what it is asking the user to trust — which means reading the manifests. The bug pattern is that 'reading the manifests' drifted into 'initializing the extensions' or 'running the probe script' over time. Each individual feature looked reasonable; the aggregate created a window where arbitrary code ran before the user was consulted.

Once an attacker lands code in the pre-trust window, they have:
- The user's full OS permissions (no sandbox has been wired yet — the sandbox is downstream of trust).
- Network access.
- Access to environment variables, including API keys and session tokens.
- Write access to `~/.claude/`, plugin directories, and hook config — they can persist.

### 3. The concrete attack shape

The path:

1. User installs a plugin, adds an MCP server, drops a skill, or wires a hook from a third-party source.
2. User runs `claude` (or equivalent).
3. The CLI's `cli.js` parses the manifest file(s) and, along the way, initializes the extension — running its init script, spawning its stdio subprocess, or evaluating a YAML frontmatter expression.
4. **The attacker's code executes.** The trust dialog has not yet rendered.
5. The trust dialog appears. The user either clicks OK (attack succeeded; trust dialog is now cover) or declines (attack already succeeded; the damage was done before the dialog).

The fix pattern, stated positively:

> Defer all extension init until after trust confirmation. Strict read-only parse of manifest fields. Manifests describe intent; they do not execute code. Signed publisher provenance required to enter the pre-trust parse path.

### 4. Mitigation checklist

This is a release gate. Work through every item. FAIL means file a ticket and block release.

#### 4.1 Boot order invariant

- [ ] The cli.js main function has an explicit phase list: `bootstrap → config-load → trust-dialog-render → user-confirm → extension-load`. The order is enforced by the call graph, not by comment.
- [ ] A lint or test asserts the order: grep for `spawn()`, `exec()`, `require()`, `eval()`, `import()` in the config-load phase; all matches are in an allow-list of vetted parsers.
- [ ] The trust dialog's render call happens before any `initializeExtension` / `startMcpServer` / `runHook` call in the call graph.
- [ ] A boot-order test fires a fake malicious plugin and asserts no side effect (no file write, no network, no subprocess) happens before the trust dialog's `onUserConfirm` fires.

#### 4.2 Manifest parse is strictly read-only

- [ ] Plugin / MCP / skill manifests are parsed by a strict JSON parser or a YAML parser with `FAILSAFE_SCHEMA` (no `!!js`, no function tags, no anchor-based code resolution).
- [ ] Manifest fields are validated against a schema (Zod / JSONSchema / similar). Unknown fields are rejected, not silently ignored.
- [ ] Template strings / string-interpolation fields in manifests are treated as literal strings at parse time. Substitution, if needed, happens at extension-load time, after trust.
- [ ] No field in any manifest triggers a `fs.readFile` that follows symlinks outside the plugin directory, reads environment variables, or resolves `~`. Plugin dirs are jailed.
- [ ] SKILL.md YAML frontmatter fields are enumerated; any field with executable potential (pre-load hooks, 'on-parse' triggers) is removed from the pre-trust parse path and moved to post-trust.

#### 4.3 Subprocess deferral

- [ ] MCP stdio transports register the spawn command at parse time but do not `spawn()` until `onUserConfirm`.
- [ ] MCP SSE / HTTP transports do not initiate the connection until `onUserConfirm`.
- [ ] Hook scripts are validated (path exists, executable bit, owner is current user) at parse time but are not executed.
- [ ] Plugin init scripts are identified at parse time but not executed; 'install probes' are static, not dynamic.

#### 4.4 Environment-variable and flag bypass

- [ ] There is no env var or flag that skips the trust dialog for normal user sessions.
- [ ] If CI images need to skip the dialog, there is a compile-time allow-list of CI user agents or sentinel files. Runtime-only env var checks are rejected.
- [ ] The trust dialog's confirm state is bound to the concrete extension set parsed this session. A user who trusts {A, B, C} cannot have D silently added — a new extension triggers a new dialog.
- [ ] No 'remember my choice forever' checkbox skips the dialog on future sessions with different extension sets. Append-only trust log, not global kill switch.

#### 4.5 Signed publisher provenance

- [ ] Every third-party extension has a signature over its manifest + content hash by a publisher key.
- [ ] The CLI's pinned keyring is shipped in the binary, not loaded from the user's disk (so an attacker on the same disk cannot inject a trusted key).
- [ ] Unsigned extensions load only after trust, and only into a restricted context (no hooks, no MCP spawn, read-only skill).
- [ ] Signature verification happens BEFORE manifest parse. A manifest whose signature does not verify is never parsed.
- [ ] Keyring updates are versioned; CLI refuses to run if the shipped keyring is older than a pinned minimum version (prevents rollback attacks).

#### 4.6 Observability

- [ ] Every extension load is logged with (timestamp, publisher, content hash, signature status, phase).
- [ ] Alerts fire on (a) an extension loaded in the pre-trust phase, (b) an unsigned extension loading, (c) a signature that failed verification.
- [ ] Logs are tamper-evident (append-only or signed). This is the same append-only commitment the rest of Claude Code's session persistence makes; extend it to boot.
- [ ] Post-incident: logs can reconstruct the exact boot sequence and extension set.

### 5. Failure mode deep dives

**Plugin init runs arbitrary code at parse time.** This is the core of the 4 CVEs. A plugin's `plugin.json` references an `init.js` and cli.js runs it during enumeration to 'discover commands'. Fix: enumerate commands by static manifest fields only. If you need dynamic commands, the plugin declares a 'post-trust-init' field and the CLI runs that after trust.

**MCP stdio transport spawns subprocess before trust prompt.** An MCP server declares itself via stdio and a `command` + `args`. cli.js calls `spawn()` to start the server, handshake, and discover its tools — all before trust. The subprocess has already run for seconds by the time the user sees the dialog. Fix: register the command but defer spawn; show the user the `command + args` in the dialog; spawn only on confirm.

**Hooks fire on installation-probe runs.** The CLI has an 'install probe' that validates a hook points at a real executable; the probe invokes the hook with a sentinel payload. Attacker authors a hook that does damage on any invocation. Fix: probes are strict metadata checks (file exists, is executable, is not a symlink out of jail); they never invoke the target.

**SKILL.md YAML frontmatter contains executable expressions.** YAML can be powerful (custom tags, anchors, !!js on node-yaml), and a skill's frontmatter has 15+ fields. A field like `preload: !!js "require(...)"` runs at parse time. Fix: `FAILSAFE_SCHEMA`, schema-validated allow-list of fields, reject unknown keys.

**Trust dialog bypassed via environment-variable injection.** Attacker sets `CLAUDE_SKIP_TRUST=1` in the user's shell rc; cli.js honors it and skips the dialog for attacker's extensions. Fix: no runtime env var bypasses trust for normal sessions. CI paths use compile-time allow-lists.

### 6. Cadence

- **Per-PR**: lint + test for the boot-order invariant. A PR that calls `spawn` / `require` / `eval` / `initializeExtension` inside the config-load phase fails CI.
- **Per-release**: run the full checklist; staff-engineer sign-off.
- **Quarterly**: reverse-engineer the boot order from the built binary to confirm nothing drifted in compile-time code.
- **Post-incident**: if any CVE is filed against the pre-trust window, every item becomes a release-blocking test until verified.

### 7. Relation to other packs

- `injection-surface-audit` — runtime content-injection audit. Pair with this pack. That pack handles attacks through fetched URLs, tool outputs, and user content; this pack handles attacks through extension load.
- `seven-safety-layers` — runtime deny-first pipeline. This pack closes the window *outside* that pipeline.

### 8. Proof of work

A PR comment or dated markdown under `docs/security/` lists every item with PASS / FAIL / WAIVED, a link to the code or ticket, and the staff engineer who signed. If evidence is missing, the item is FAIL, not PASS.

Sourced from VILA-Lab/Dive-into-Claude-Code (CC-BY-NC-SA-4.0, arXiv 2604.14228). The pre-trust execution window framing and the tying of patched CVEs to a single root cause are paraphrased with citation from README §Key Highlights and the Safety and Permissions §Pre-trust execution window excerpt.

## Evaluation Checklist

- Boot-order invariant enforced by call graph + test: trust dialog renders before any third-party extension loads.
- Manifest parsers use strict schemas (FAILSAFE_SCHEMA for YAML, Zod/JSONSchema for JSON) with unknown-field rejection.
- MCP stdio transports defer spawn() until after onUserConfirm; registration at parse is metadata-only.
- Hooks validated at parse, executed only post-trust; install probes never invoke the hook target.
- No runtime env-var or flag bypass for the trust dialog in normal user sessions; CI paths use compile-time allow-lists.
- Publisher signatures verified BEFORE manifest parse; pinned keyring shipped in binary, not loaded from disk.
- Boot-sequence logs are append-only and tamper-evident; alerts fire on pre-trust loads, unsigned loads, and signature failures.
- A malicious-plugin boot-order test in CI asserts no file write, network call, or subprocess spawns before onUserConfirm.

## Failure Modes

- **[STAFF] Plugin executes arbitrary code at CLI startup, before the trust dialog — 4 CVEs in this shape**
  - Trigger: cli.js 'enumerates' plugin commands by running the plugin's init.js during config-load, rather than reading a static manifest field
  - Prevention: Enumerate commands from static manifest fields only; declare any dynamic commands in a 'post-trust-init' field that the CLI calls after onUserConfirm. CI test boots a malicious plugin and asserts no pre-trust side effects. (see: injection-surface-audit, seven-safety-layers)
- **[STAFF] MCP stdio transport subprocess runs for seconds before the user sees any trust prompt**
  - Trigger: cli.js calls spawn() on the MCP command + args to handshake and discover tools during config-load, so the user's 'trust' decision is cover for code that already ran
  - Prevention: Register the command + args at parse; show them verbatim in the trust dialog; spawn only on confirm. Handshake + tool discovery happen post-trust.
- **[STAFF] Installing a hook triggered an arbitrary payload before the hook was ever 'invoked' by the agent**
  - Trigger: An 'install probe' validated the hook by running it with a sentinel — any code in the hook executed on probe, regardless of runtime conditions
  - Prevention: Probes are strict metadata checks (file exists, executable bit, owner, not a symlink out of jail). Probes never invoke the hook target. Target invocation is gated by the permission system at runtime, not by an install-time probe.
- **[SR] A skill's YAML frontmatter ran a node-yaml !!js expression at parse time and exfiltrated env vars**
  - Trigger: SKILL.md YAML parsed with a permissive schema that resolved custom tags; 15+ frontmatter fields included one with executable potential
  - Prevention: Use FAILSAFE_SCHEMA; schema-validate a closed allow-list of frontmatter fields; reject unknown keys. No tags, no anchors that reference code paths, no template-string evaluation at parse.
- **[STAFF] User with CLAUDE_SKIP_TRUST=1 in their shell rc was silently bypassing the trust dialog for all extensions**
  - Trigger: cli.js honored an env var shortcut intended for CI images but with no runtime guard restricting it to CI
  - Prevention: Remove all runtime env-var and flag bypasses for the trust dialog. CI images get compile-time allow-listed user agents or sentinel files; runtime-only env vars cannot skip trust.
- **[STAFF] After patching one CVE, a regression restored the pre-trust extension-init path under a different name**
  - Trigger: Fix was a surgical patch on one call site; no boot-order invariant test in CI; a subsequent refactor re-introduced the shape
  - Prevention: Boot-order invariant enforced by a lint AND a CI test that boots a fake malicious plugin. No pre-trust side effects; test is release-blocking.

## Transfer Matrix

_No measured cross-model transfer data._

## Telemetry

_No telemetry recorded._

## Security Review

- Injection surface: **high**
- Tool allow-list: _none specified_
- Last scanned: 2026-04-19

### Known issues
- This is a meta-security pack; it ships no runtime code of its own, so it has no tool surface to audit.
- The pack cannot enumerate all future pre-trust window shapes — treat the checklist as a floor, not a ceiling. Novel extension mechanisms need fresh analysis.
- Downstream uses of this pack's text must preserve CC-BY-NC-SA-4.0 attribution and NC-SA terms for any verbatim excerpts from Dive into Claude Code.

## Compares With

| Compared to | Axis | Winner | Note |
| --- | --- | --- | --- |
| `injection-surface-audit` | accuracy | tie | Different surfaces: injection-surface-audit covers runtime content attacks (fetched URLs, tool outputs, user-submitted text). This pack covers load-time code execution in the pre-trust window. Run both; neither replaces the other. |
| `seven-safety-layers` | complexity | other | seven-safety-layers documents the runtime deny-first pipeline; this pack documents the shape that lives OUTSIDE that pipeline. Simpler scope here (one surface), but depends on seven-safety-layers for post-trust enforcement. |

## Related Packs

- `injection-surface-audit`
- `seven-safety-layers`

## Changelog

### 0.1.0 — 2026-04-19
_Seed pack — first release. Targets the pre-trust execution window documented in VILA-Lab/Dive-into-Claude-Code README §Key Highlights and Safety §Pre-trust execution window. Four patched CVEs share this root cause per the paper._

**Added**
- Statement of the pre-trust execution window as a distinct attack surface (not prompt injection)
- Six-section mitigation checklist (boot order, manifest parse, subprocess deferral, env-var bypass, signed provenance, observability)
- Five concrete attack-shape deep-dives tied to patched CVE root causes per the paper
- CI-enforceable boot-order invariant lint + test plan
- Cadence: per-PR, per-release, quarterly, post-incident

## Sources

- [VILA-Lab — Dive into Claude Code (CC-BY-NC-SA-4.0)](https://github.com/VILA-Lab/Dive-into-Claude-Code) — Primary source. README §Key Highlights: '4 CVEs Reveal a Pre-Trust Window — Extensions execute before the trust dialog appears.' Paraphrased with citation under CC-BY-NC-SA-4.0; verbatim excerpts preserve the NC-SA terms.
- [Dive into Claude Code — paper (arXiv 2604.14228)](https://arxiv.org/abs/2604.14228) — Academic reference. Pre-trust execution window treated as a structural gap outside the deny-first pipeline. Cite in downstream work.
- [Dive into Claude Code — Safety and Permissions §Pre-trust execution window](https://github.com/VILA-Lab/Dive-into-Claude-Code/blob/main/README.md#safety-and-permissions) — Section excerpt: '2 patched CVEs share this root cause -- hooks and MCP servers execute during initialization before the trust dialog appears, creating a structurally privileged attack window outside the deny-first pipeline.'
- [Anthropic — Claude Code Auto Mode](https://www.anthropic.com/engineering/claude-code-auto-mode) — Vendor context on permission-system design; grounds the 'deny-first runtime pipeline' that this pack augments at boot time.
- [OWASP — Top 10 for LLM Applications](https://owasp.org/www-project-top-10-for-large-language-model-applications/) — Industry vocabulary for LLM-app vulnerability classes. Supply chain and insecure plugin design categories cover the runtime shape of these attacks; pre-trust window is the load-time variant.

## Examples

- [VILA-Lab/Dive-into-Claude-Code — README (pre-trust window excerpt)](https://github.com/VILA-Lab/Dive-into-Claude-Code#safety-and-permissions) (external)
- [arXiv 2604.14228 — Dive into Claude Code](https://arxiv.org/abs/2604.14228) (external)
