Skip to content

fix(mcp): phase agents receive the user's entire Claude Desktop config — capability over-grant + literal secrets in argv #936

Description

@admarble

Symptom

With settings.run.mcp enabled (the default), every headless phase agent
receives the user's ENTIRE Claude Desktop config:
getMcpServersConfig() (src/lib/system.ts:179) reads
claude_desktop_config.json and claude-code.ts:119 passes the whole
object to the SDK query().

Two consequences, both observed in the 2026-08-10 overnight run on
ad-motion:

  1. Capability over-grant. Autonomous phase agents inherited an
    email-sending MCP server (SMTP send-as), a Stripe server, a Google
    Sheets server with a service-account file, a GitHub server with a PAT,
    and a filesystem server rooted at ~/Desktop + ~/Documents. A phase
    agent implementing a GitHub issue needs none of these — and fix(containment): a phase agent killed its own orchestrator — no Bash signal guard, no process-group isolation, machine re-prompts read as human consent #934's
    incident shows phase agents will use surprising capabilities when they
    misread a situation.

  2. Secrets in argv. The SDK serializes the merged config into
    --mcp-config <json> on the claude child's command line. Claude
    Desktop configs cannot use ${VAR} references (the desktop app does
    not expand them), so they contain literal secrets — which are then
    visible to ANY local process via ps, and get captured into agent
    transcripts whenever an agent runs ps aux (this is exactly how live
    credentials ended up in the fix(containment): a phase agent killed its own orchestrator — no Bash signal guard, no process-group isolation, machine re-prompts read as human consent #934 incident transcript).

Proposed fix

Phases should get an allowlist, not a passthrough:

  • Default: inject ONLY the sequant MCP server (and anything the project's
    own .mcp.json declares — that file is designed to be secret-free).
  • Never read claude_desktop_config.json for phase execution. It is a
    different trust domain: servers a human uses interactively in the
    desktop app, not servers an autonomous agent should hold.
  • If passthrough is ever wanted, make it opt-in per server:
    settings.run.mcpAllowlist: ["name", ...].

AC

  • Phase agent argv contains no MCP servers beyond sequant + project
    .mcp.json entries (test: spawn a phase with a populated fake
    desktop config; assert its --mcp-config excludes those servers)
  • No secret-bearing env values from the desktop config appear in the
    phase process argv
  • Docs: run.mcp documented as sequant-server injection, not
    desktop passthrough

Related: #161 (introduced the passthrough), #934 (containment incident
whose transcript captured the leaked credentials), #395.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingplannedImplementation plan approvedready-for-reviewReady for code review

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions