Skip to content

claude.ai account connectors (Gmail, Drive, …) leak into every sandbox — suppress by default, opt in per-instance #129

Description

@rappdw

Summary

Every sandy sandbox is pre-authenticated to the user's entire Claude account, which means every claude.ai account connector the user has ever enabled (Gmail, Google Drive, …) is silently reachable from every sandbox — including untrusted-repo sessions. A prompt-injection or a merely-wrong agent in any workspace can read the user's inbox/Drive with zero additional prompt. This is ambient authority that directly contradicts sandy's per-project isolation model ("an untrusted repo shouldn't be able to reach your stuff").

Discovered in the field: a maintainer noticed all sandy instances could read their Gmail after connecting it once via claude.ai.

Evidence (reproduced in-container)

The seeded ~/.claude.json inside a live sandbox contains:

"claudeAiMcpEverConnected": ["claude.ai Gmail", "claude.ai Google Drive"]

There is no mcpServers entry and no workspace .mcp.json — the connectors are not local MCP servers. They are account entitlements surfaced to any Claude Code client authenticated as the account.

Root cause — it's account-scoped, not config-scoped

  1. load_credentials() (sandy ~L7853) reads the host's full-scope Claude OAuth token (~/.claude/.credentials.json or macOS Keychain).
  2. It's written to a tmpdir and bind-mounted into every container (sandy ~L8140-8145), fresh each launch.
  3. claude.ai account connectors ride on that account OAuth. Being logged into the account == having the connectors. So every sandbox inherits them.

Consequences for the fix:

  • Stripping mcpServers / .claude.json connector metadata does nothing — the access isn't in the config, it's in the token's account entitlement. (The .claude.json seed already strips projects but copies claudeAiMcpEverConnected and everything else through.)
  • The knob has to be a Claude Code setting that suppresses account/remote connectors, applied per instance.

Why sandy's existing defenses miss this

Sandy defends network egress (proxy), host-config injection (:ro mounts), and per-project credential sandboxes — but the Claude OAuth token is intentionally mounted whole into every sandbox (that's how the agent authenticates), and connectors were an invisible rider on it. None of the protected-files / egress / approval machinery covers "the token grants inbox access."

Proposed posture

Suppress account connectors by default; opt in per-instance. Matches sandy's "a repo may make the sandbox tighter, never looser" rule and the existing weakening-value approval model.

New config key: SANDY_CLAUDE_CONNECTORS0 (default: connectors suppressed) | 1 (expose account connectors for this instance).

  • Tier: weakening=1 grants an untrusted session reach to Gmail/Drive, so from a workspace .sandy/config it must go through the per-workspace approval prompt (same class as SANDY_EGRESS_NO_ISOLATION=1). Free from host ~/.sandy/config; settable per-launch via env. This gives the requested per-instance/per-project control: turn it on only for the workspace where you actually want the agent to touch your mail.

Confirmed Claude Code knob

Per claude-code-guide (verify the exact key against the installed Claude Code version before wiring — key names drift between releases):

  • disableClaudeAiConnectors: true in settings.json — the canonical "disable ALL account connectors" switch. Env-var equivalent ENABLE_CLAUDEAI_MCP_SERVERS=false.
  • Precedence is any-source-true wins: a true at any scope (user / project / local / managed) disables connectors and cannot be re-enabled by a lower scope. Useful — but it means to expose connectors when SANDY_CLAUDE_CONNECTORS=1, sandy must ensure nothing it seeds sets true (write false, and make it a managed / always-overwritten key so a host ~/.claude/settings.json false can't silently defeat the default).
  • Related finer-grained knobs (not needed for v1 but noted): disabledMcpServers (denylist by display name, e.g. "claude.ai Slack"), --mcp-config servers are exempt from the switch.

Mechanism

  • Add disableClaudeAiConnectors to sandy's managed settings.json keys (the always-re-overwritten set — like permissions.defaultMode / extraKnownMarketplaces, not the only-if-absent set like teammateMode), with value !SANDY_CLAUDE_CONNECTORS. Default SANDY_CLAUDE_CONNECTORS=0 → seed disableClaudeAiConnectors: true → connectors off. SANDY_CLAUDE_CONNECTORS=1 → seed false → connectors on.
  • Defense-in-depth: also strip claudeAiMcpEverConnected from the .claude.json seed (host metadata, meaningless-and-leaky in the sandbox) regardless of the knob.

Open questions (to resolve before implementing)

  • Confirm disableClaudeAiConnectors is the real key name in the installed Claude Code version (a claude docs check or a live /mcp before/after test in a sandbox).
  • Codex/Gemini/OpenCode: do any have an analogous account-connector rider? (Gemini has its own extensions/SANDY_GEMINI_EXTENSIONS; likely Claude-only for this specific issue — scope the fix to claude, document the limit.)
  • Should the default flip be unconditional off or off for untrusted / on for host-configured trusted workspaces? Leaning unconditional-off for a clean, predictable default.
  • Consider forwarding ENABLE_CLAUDEAI_MCP_SERVERS=false as belt-and-suspenders alongside the settings.json key.

Acceptance criteria

  • Fresh sandbox, default config: /mcp (or /connectors) shows no account connectors.
  • SANDY_CLAUDE_CONNECTORS=1: account connectors present.
  • Setting =1 from a workspace .sandy/config triggers the passive-privileged approval prompt; headless/non-TTY fails closed (drops it).
  • claudeAiMcpEverConnected no longer present in the seeded .claude.json.
  • test/run-tests.sh structural assertion (seed strips the key; the disable setting is written unless the knob is 1); doc updates (README + CLAUDE.md + --print-schema metadata row).

Notes

  • Claude-only in v1 (the account-connector rider is a Claude Code phenomenon). State the limit; don't oversell coverage for the other agents.
  • Relevant Claude Code internals seen in the seeded config: feature flag tengu_claudeai_mcp_connectors, key claudeAiMcpEverConnected, oauthAccount block (all account-level).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions