Skip to content

Explicit MCP exemption policy in bootstrap_gate (audit-first; PR #641 R2-S-2 follow-up) #644

@michael-wojcik

Description

@michael-wojcik

Background

During PR #641 Round-2 blind review (R2-S-2 finding), security-engineer-r2 flagged that bootstrap_gate.py allows MCP tools by default via an implicit prefix-match:

# pact-plugin/hooks/bootstrap_gate.py (~L170)
# MCP tools always allowed (external integrations)
if isinstance(tool_name, str) and tool_name.startswith("mcp__"):
    return None

This is correct under current threat models (MCP tools are external integrations the user explicitly granted), but the policy is implicit — there's no allowlist, no logging, and no way for a security review to assert "exactly these MCP tools bypass the bootstrap gate".

Threat scenarios

  1. Untrusted MCP server registration — a user registers an MCP server, perhaps unintentionally trusting an untrusted source. That server's tools all bypass bootstrap-gate enforcement. The user never sees a security boundary for MCP-vs-non-MCP.
  2. MCP tool name collision — a malicious tool registers as mcp__bootstrap_unblock (or similar). Prefix-match exempts it without further inspection.
  3. Audit gap — security-engineer cannot enumerate "which tools bypass bootstrap-gate" by reading bootstrap_gate.py source. The set is whatever's currently registered in MCP — runtime-determined.

Proposal

Option A — explicit allowlist with config

# pact-plugin/hooks/bootstrap_gate.py
_MCP_ALLOWLIST_PATH = Path.home() / ".claude" / "pact-mcp-bootstrap-allowlist.json"
# Format: {"servers": ["claude-in-chrome", "computer-use", ...], "enforcement": "allow|deny|log"}

def _check_mcp_tool(tool_name: str) -> str | None:
    if not tool_name.startswith("mcp__"):
        return None
    server = tool_name.split("__")[1] if "__" in tool_name[5:] else None
    config = _load_mcp_allowlist()  # cached
    if config["enforcement"] == "deny":
        return _DENY_REASON_MCP
    if config["enforcement"] == "log":
        _audit_log(tool_name, "MCP bypass")  # journal event
        return None
    if server in config["servers"]:
        return None
    return _DENY_REASON_MCP_NOT_ALLOWLISTED

Default config: enforcement: "allow" with empty servers list (current behavior preserved). Users can opt into stricter modes.

Option B — log-only (audit-first)

# pact-plugin/hooks/bootstrap_gate.py
if tool_name.startswith("mcp__"):
    # Allow for now; log the bypass to journal so audits enumerate
    _journal_mcp_bypass(tool_name)
    return None

Journal entry creates an enumerable record without changing default behavior. Cheaper to implement, less invasive.

Recommendation

Start with Option B (audit-first). Adding logging gives security review the enumerable record that's currently missing. If concrete attack scenarios materialize, extend to Option A.

Acceptance criteria

  • MCP-bypass events emit a mcp_bypass journal event with tool_name, timestamp, session_id.
  • session_journal.py accepts the new event type.
  • Test asserts a journal entry is written when mcp__* tool bypasses bootstrap-gate.
  • Documentation in bootstrap_gate.py docstring explicitly notes "MCP tools bypass; bypasses are logged to journal for audit".

Cross-references

  • PR Restore session-startup ritual (#628) #641 R2-S-2 (security-engineer-r2 finding, deferred to follow-up)
  • bootstrap_gate.py L170 — current implicit allow
  • session_journal.py — canonical event-persistence layer (per pinned project rule)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions