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
load_credentials() (sandy ~L7853) reads the host's full-scope Claude OAuth token (~/.claude/.credentials.json or macOS Keychain).
- It's written to a tmpdir and bind-mounted into every container (sandy ~L8140-8145), fresh each launch.
- 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_CONNECTORS — 0 (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)
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).
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.jsoninside a live sandbox contains:There is no
mcpServersentry 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
load_credentials()(sandy ~L7853) reads the host's full-scope Claude OAuth token (~/.claude/.credentials.jsonor macOS Keychain).Consequences for the fix:
mcpServers/.claude.jsonconnector metadata does nothing — the access isn't in the config, it's in the token's account entitlement. (The.claude.jsonseed already stripsprojectsbut copiesclaudeAiMcpEverConnectedand everything else through.)Why sandy's existing defenses miss this
Sandy defends network egress (proxy), host-config injection (
:romounts), 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_CONNECTORS—0(default: connectors suppressed) |1(expose account connectors for this instance).=1grants an untrusted session reach to Gmail/Drive, so from a workspace.sandy/configit must go through the per-workspace approval prompt (same class asSANDY_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: trueinsettings.json— the canonical "disable ALL account connectors" switch. Env-var equivalentENABLE_CLAUDEAI_MCP_SERVERS=false.trueat any scope (user / project / local / managed) disables connectors and cannot be re-enabled by a lower scope. Useful — but it means to expose connectors whenSANDY_CLAUDE_CONNECTORS=1, sandy must ensure nothing it seeds setstrue(writefalse, and make it a managed / always-overwritten key so a host~/.claude/settings.jsonfalsecan't silently defeat the default).disabledMcpServers(denylist by display name, e.g."claude.ai Slack"),--mcp-configservers are exempt from the switch.Mechanism
disableClaudeAiConnectorsto sandy's managed settings.json keys (the always-re-overwritten set — likepermissions.defaultMode/extraKnownMarketplaces, not the only-if-absent set liketeammateMode), with value!SANDY_CLAUDE_CONNECTORS. DefaultSANDY_CLAUDE_CONNECTORS=0→ seeddisableClaudeAiConnectors: true→ connectors off.SANDY_CLAUDE_CONNECTORS=1→ seedfalse→ connectors on.claudeAiMcpEverConnectedfrom the.claude.jsonseed (host metadata, meaningless-and-leaky in the sandbox) regardless of the knob.Open questions (to resolve before implementing)
disableClaudeAiConnectorsis the real key name in the installed Claude Code version (aclaudedocs check or a live/mcpbefore/after test in a sandbox).SANDY_GEMINI_EXTENSIONS; likely Claude-only for this specific issue — scope the fix to claude, document the limit.)ENABLE_CLAUDEAI_MCP_SERVERS=falseas belt-and-suspenders alongside the settings.json key.Acceptance criteria
/mcp(or/connectors) shows no account connectors.SANDY_CLAUDE_CONNECTORS=1: account connectors present.=1from a workspace.sandy/configtriggers the passive-privileged approval prompt; headless/non-TTY fails closed (drops it).claudeAiMcpEverConnectedno longer present in the seeded.claude.json.test/run-tests.shstructural assertion (seed strips the key; the disable setting is written unless the knob is1); doc updates (README + CLAUDE.md +--print-schemametadata row).Notes
tengu_claudeai_mcp_connectors, keyclaudeAiMcpEverConnected,oauthAccountblock (all account-level).