You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The one thing worth knowing: your Anthropic OAuth access and refresh tokens are on disk here, readable by any process running as your user in this container. They can't reach Gmail from inside this VLAN, but pypi.org and github.com are allowlisted, so exfiltration through one of those and reuse elsewhere is the realistic residual risk. That file is the one credential actually worth protecting on this machine.
Exactly right. Sandy mounts the host .credentials.json — access token and refresh token — ephemerally into every container (load_credentials() → tmpdir → bind mount, sandy ~L7853/8140). The refresh token is the crown jewel: an exfiltrated refresh token is permanent, renewable account access until manually revoked, and permissive-mode egress allowlists (pypi/github) are plausible exfil channels a poisoned dependency or injected agent could use.
The gap vs. what's already tracked
Adopt nono's phantom-token credential-proxy pattern for Phase-1 broker-not-mount #121 (phantom-token broker) is the endgame: the token never enters the container at all. But it's a large feature — host-side daemon, localhost reverse proxy, phantom tokens, cmd:// capture, a TLS-termination decision. Its Phase-1 A1 slice names the "OAuth refresh → short-lived access token" case as highest-value, but delivering it is gated on the whole broker spike.
Neither ships a cheap, near-term, per-workspace way to shrink the token's blast radius for a workspace you actively distrust. This issue is that slice.
Proposed: SANDY_SUSPICIOUS — a hardened-credential posture, no broker required
An umbrella opt-in for "I don't trust this code" that composes existing + new tightening, all strengthening (so passive-safe to turn on from a workspace .sandy/config — a repo may make itself more suspicious, never less):
Strip the refresh token (the core win). Before mounting .credentials.json, rewrite it to drop claudeAiOauth.refreshToken (and any other long-lived fields), mounting only the short-TTL access token. Sandy already parses claudeAiOauth.expiresAt (token_needs_refresh), so this is a small JSON rewrite in the existing tmpdir path — no host helper, no TLS termination, no Adopt nono's phantom-token credential-proxy pattern for Phase-1 broker-not-mount #121 broker.
Blast radius: permanent-and-renewable → the remaining access-token lifetime (hours). An exfiltrated copy expires at expiresAt and cannot be refreshed. This is the single highest-leverage, lowest-cost mitigation for the stated residual.
Trade-off (document it): in-session token refresh fails once the access token expires → the agent must /login again or the long session ends. Acceptable-to-desirable for a deliberately-suspicious session (you don't want a marathon run against untrusted code anyway). For a fresh access token at launch, sandy can refresh host-side first (it already can) and mount the newest one.
Alternative / complementary credential modes (menu, pick for v1)
Disposable API key. If the user supplies a scoped/revocable ANTHROPIC_API_KEY for the suspicious workspace, prefer it and mount no OAuth creds at all — clean compartmentalization, instantly revocable without touching the primary account. (Sandy already supports API-key auth; this is just "prefer it + skip the OAuth mount under SANDY_SUSPICIOUS.")
Consider making SANDY_SUSPICIOUS the documented composition point so future hardening (seccomp Unix-socket block, SANDY_RUNTIME=runsc, tool-audit on) can bundle under it.
This is the pre-broker, no-new-daemon delivery of #121's A1 "OAuth refresh→short-lived access token" idea, scoped to the suspicious-workspace use case and shippable independently. #121 remains the endgame (token never in-container). Ship this first; it captures most of the value at a fraction of the cost, and de-risks the #121 A1 design by proving the short-lived-token flow end-to-end.
Acceptance criteria
SANDY_SUSPICIOUS=1: mounted .credentials.json contains an access token but norefreshToken; agent can call the API; after expiresAt refresh fails (verified, not just asserted).
The residual
Field observation, in a maintainer's own words:
Exactly right. Sandy mounts the host
.credentials.json— access token and refresh token — ephemerally into every container (load_credentials()→ tmpdir → bind mount, sandy ~L7853/8140). The refresh token is the crown jewel: an exfiltrated refresh token is permanent, renewable account access until manually revoked, and permissive-mode egress allowlists (pypi/github) are plausible exfil channels a poisoned dependency or injected agent could use.The gap vs. what's already tracked
cmd://capture, a TLS-termination decision. Its Phase-1 A1 slice names the "OAuth refresh → short-lived access token" case as highest-value, but delivering it is gated on the whole broker spike.Neither ships a cheap, near-term, per-workspace way to shrink the token's blast radius for a workspace you actively distrust. This issue is that slice.
Proposed:
SANDY_SUSPICIOUS— a hardened-credential posture, no broker requiredAn umbrella opt-in for "I don't trust this code" that composes existing + new tightening, all strengthening (so passive-safe to turn on from a workspace
.sandy/config— a repo may make itself more suspicious, never less):.credentials.json, rewrite it to dropclaudeAiOauth.refreshToken(and any other long-lived fields), mounting only the short-TTL access token. Sandy already parsesclaudeAiOauth.expiresAt(token_needs_refresh), so this is a small JSON rewrite in the existing tmpdir path — no host helper, no TLS termination, no Adopt nono's phantom-token credential-proxy pattern for Phase-1 broker-not-mount #121 broker.expiresAtand cannot be refreshed. This is the single highest-leverage, lowest-cost mitigation for the stated residual./loginagain or the long session ends. Acceptable-to-desirable for a deliberately-suspicious session (you don't want a marathon run against untrusted code anyway). For a fresh access token at launch, sandy can refresh host-side first (it already can) and mount the newest one.disableClaudeAiConnectors: true(claude.ai account connectors (Gmail, Drive, …) leak into every sandbox — suppress by default, opt in per-instance #129) regardless ofSANDY_CLAUDE_CONNECTORS.Alternative / complementary credential modes (menu, pick for v1)
ANTHROPIC_API_KEYfor the suspicious workspace, prefer it and mount no OAuth creds at all — clean compartmentalization, instantly revocable without touching the primary account. (Sandy already supports API-key auth; this is just "prefer it + skip the OAuth mount underSANDY_SUSPICIOUS.")Tier & surface
SANDY_SUSPICIOUS(bool) — passive-safe (strengthening). Also settable host/env.sandy-session.json(mirrors theeffortfield precedent, and Adopt nono's phantom-token credential-proxy pattern for Phase-1 broker-not-mount #121 item 4) so a run's protection level is provable after the fact: e.g."cred_mode": "access-token-only" | "api-key" | "full".SANDY_SUSPICIOUSthe documented composition point so future hardening (seccomp Unix-socket block,SANDY_RUNTIME=runsc, tool-audit on) can bundle under it.Relationship to #121
This is the pre-broker, no-new-daemon delivery of #121's A1 "OAuth refresh→short-lived access token" idea, scoped to the suspicious-workspace use case and shippable independently. #121 remains the endgame (token never in-container). Ship this first; it captures most of the value at a fraction of the cost, and de-risks the #121 A1 design by proving the short-lived-token flow end-to-end.
Acceptance criteria
SANDY_SUSPICIOUS=1: mounted.credentials.jsoncontains an access token but norefreshToken; agent can call the API; afterexpiresAtrefresh fails (verified, not just asserted).SANDY_SUSPICIOUS(ties to claude.ai account connectors (Gmail, Drive, …) leak into every sandbox — suppress by default, opt in per-instance #129).ANTHROPIC_API_KEYis present, OAuth creds are not mounted at all.sandy-session.jsonrecordscred_mode..sandy/config(strengthening → no approval prompt);--print-schemametadata row; README + CLAUDE.md;run-tests.shstructural assertion (strip logic + session-marker field).Honest limits