Agent Workspace
← Back to catalog
Communitysecurityv0.1.0Recommended

CVE: The Pre-Trust Execution Window

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

Agent WorkspaceSigned (unverified)Verified publisher·Updated 2026-04-19·~0 installs this month · saved ~0 tokens

Install

npx attrition-sh pack install cve-pre-trust-window
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.

Raw Markdown

Machine-readable body for agent ingestion or copy/paste.

Download as .md

Telemetry

Not yet measured

Skipping this saves ~52,000 tokens / 140 min of research.

Methodology

Measured 2026-04-19

Prompted a fresh Claude Sonnet 4.6 with 'what is the load-time code-execution window that occurs in CLI harnesses that load third-party extensions before rendering a trust dialog, and how do you close it?'. Measured tokens until the output named the pre-trust window as a distinct attack surface from prompt injection, identified the four concrete shapes (plugin init, MCP stdio spawn, hook install probe, YAML frontmatter execution) and the env-var bypass class, and produced a checklist of boot-order invariant, read-only manifest parse, subprocess deferral, signature-before-parse, and append-only boot logging. Averaged over 3 runs.

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.

Fit and expected payoff

When this pack earns its extra structure, when to skip it, and what it should improve.

Situations where this pack earns its extra structure.

  • 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.

Keeps the pack from becoming a default hammer.

  • 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.

Expected outcomes if implemented well.

  • 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

Smallest useful starting point.

## 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

Complete natural-language instruction set.

Show 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

These checks should pass before you consider the pattern production-ready.

  • 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.

Common failure modes

Every check below traces back to a specific production failure. Read as: "I would think about X because in production Y can happen."

  • 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 also
    injection-surface-auditseven-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.
  • Senior

    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.

How this pack stacks up

Head-to-head notes vs alternative patterns.

AlternativeAxisWinnerNote
accuracyTieDifferent 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.
complexityAlternativeseven-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.

How this pack connects

Injection surface, allow-list, and known issues

High

2026-04-19

No tool permissions granted.

  • 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.

Version history

  1. v0.1.0

    2026-04-19

    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

    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.

Official docs and implementation references

Reference implementations