claudebox/.planning/phases/04-auth-passthrough/04-01-PLAN.md
2026-04-10 09:11:05 +00:00

11 KiB
Raw Blame History

plan phase wave depends_on files_modified requirements autonomous must_haves
1 04 1
claudebox.sh
AUTH-01
AUTH-02
true
truths artifacts key_links
claudebox launches successfully when ~/.claude/.credentials.json exists on the host
OAuth token refresh can write back to the credentials file (read-write mount)
claudebox launches without error when ~/.claude/.credentials.json does not exist
ANTHROPIC_API_KEY is passed into the sandbox when set on the host
The audit screen shows all env vars in a single unified list with [~]/[>]/[+] prefixes
The audit screen shows a Mounts section and a Network section after the env list
The --dry-run output mirrors the credential bind when the file exists
path provides contains
claudebox.sh Credential mount logic, updated print_audit, updated --dry-run block credentials.json
from to via
claudebox.sh credential bind block exec bwrap call (line ~327) --bind $HOME/.claude/.credentials.json $HOME/.claude/.credentials.json
from to via
claudebox.sh credential bind block --dry-run display block (line ~292) mirrored echo of credential bind
from to via
print_audit AUDIT_SANDBOX_KEYS / AUDIT_HOST_KEYS / AUDIT_EXTRA_KEYS arrays [~]/[>]/[+] prefix loop

Plan 1: Credential Mount + Audit Redesign

Goal

Mount ~/.claude/.credentials.json read-write into the sandbox and rewrite the pre-launch audit to a unified env/mounts/network display.

Context

Claude Code stores OAuth tokens in ~/.claude/.credentials.json. Without mounting this file, users must re-authenticate every time. The mount must be read-write because OAuth refresh writes new tokens back to the file. When ANTHROPIC_API_KEY is set on the host, it is already passed through (line 204 of claudebox.sh) — no changes needed for that path.

The current audit shows three separate sections (Sandbox-generated / Host allowlisted / Extra). The redesign collapses these into one list with [~]/[>]/[+] prefixes (accessible without color), adds a Mounts section showing active filesystem binds, and adds a Network section (placeholder showing "full (host network)" until Phase 6).

<threat_model>

Threat Model

Assets:

  • ~/.claude/.credentials.json — OAuth tokens granting access to the user's Claude subscription
  • ANTHROPIC_API_KEY — API key granting billable Claude API access

Threat actors:

  • Malicious code running inside the sandbox that could exfiltrate credentials via network or file writes
  • A compromised Claude Code session writing a crafted .credentials.json back to the host

Attack vectors:

  • Sandbox code reads .credentials.json and exfiltrates tokens over the network (full host network in Phase 4)
  • Sandbox code overwrites .credentials.json with attacker-controlled content, poisoning the host credential store
  • Path traversal: if the bind target were $HOME/.claude/ (directory), sandbox code could write new files into ~/.claude on the host

Mitigations:

  • Bind is file-scoped (--bind .credentials.json .credentials.json), NOT directory-scoped — sandbox cannot create new files in the host ~/.claude directory via this mount
  • Read-write is required for legitimate OAuth refresh; this is an accepted trade-off documented in D-02
  • ANTHROPIC_API_KEY is masked in audit output by mask_value (already implemented)
  • Phase 6 network isolation will allow users to restrict exfiltration surface; Phase 4 accepts full-network risk consistent with existing behavior

Residual risk:

  • A malicious prompt could cause Claude to read and exfiltrate the OAuth token over the network. Accepted: this is equivalent risk to the API key already in the sandbox. Phase 6 network tiers reduce this surface.
  • OAuth token can be overwritten by sandbox code. Accepted: the file must be writable for refresh to work (D-02). The host file is the user's own credential file. </threat_model>

Tasks

<read_first>

  • claudebox.sh lines 284350 — the --dry-run display block and exec bwrap call; credential bind goes in both places
  • claudebox.sh lines 98103 — CWD and ~/.claudebox setup pattern to follow </read_first>

After the mkdir -p "$HOME/.claudebox" call and before the print_audit invocation, add a block that:

  1. Sets CREDS_FILE="$HOME/.claude/.credentials.json"
  2. Checks if [[ -f "$CREDS_FILE" ]]; then sets CREDS_MOUNT=true; else CREDS_MOUNT=false
  3. Adds --bind "$CREDS_FILE" "$HOME/.claude/.credentials.json" to the exec bwrap call (after the --symlink "$HOME/.claudebox" "$HOME/.claude" line) when CREDS_MOUNT=true
  4. Mirrors the same conditional bind in the --dry-run display block (after the symlink echo)

Bwrap mount ordering note: The credential file bind must come AFTER the --symlink "$HOME/.claudebox" "$HOME/.claude" line. bwrap processes mounts in order; the symlink creates ~/.claude → ~/.claudebox, and the subsequent file bind layers .credentials.json on top of it inside the sandbox. This matches the established pattern from the code context.

Do NOT use --ro-bind — the mount must be read-write for OAuth token refresh (D-02).

Do NOT add any warning or error output when the file is absent — skip silently (D-03).

Always add the bind when the file exists, even if ANTHROPIC_API_KEY is also set (D-04).

<acceptance_criteria>

  • grep -n 'CREDS_FILE' claudebox.sh shows the variable declared as CREDS_FILE="$HOME/.claude/.credentials.json"
  • grep -n 'CREDS_MOUNT' claudebox.sh shows both the conditional set and usage in bwrap exec
  • grep -n 'credentials.json' claudebox.sh shows the bind appears in BOTH the exec bwrap block AND the --dry-run block
  • The bind uses --bind (not --ro-bind) — grep 'ro-bind.*credentials' claudebox.sh returns nothing
  • grep -A2 'CREDS_MOUNT' claudebox.sh shows the condition is [[ -f "$CREDS_FILE" ]] — file existence check, not variable existence
  • No echo or >&2 output is produced when the credentials file is absent — the skip is completely silent </acceptance_criteria>

<read_first>

  • claudebox.sh lines 171263 — the audit data structures (AUDIT_SANDBOX_KEYS, AUDIT_HOST_KEYS, AUDIT_EXTRA_KEYS arrays and associated maps) and the current print_audit function; the rewrite must consume the same data structures
  • claudebox.sh lines 6576 — ANSI color variable definitions (BOLD, RESET, DIM, CYAN, YELLOW, GREEN, RED already defined)
  • claudebox.sh lines 7891 — mask_value function signature and behavior; must be called for all env var values in the new display </read_first>

Rewrite the print_audit function (lines 227263) to produce the new unified format per D-06, D-07, D-08, D-09, D-10.

New print_audit structure:

=== Sandbox Environment ===

  [~] HOME=/home/user           ← green [~] prefix, sandbox-generated
  [~] USER=user
  [~] PATH=                     ← PATH gets special multiline treatment (same as before)
        /nix/store/.../bin
  [>] TERM=xterm-256color       ← yellow [>] prefix, host allowlisted (only if set)
  [>] ANTHROPIC_API_KEY=abc123...xyz  ← masked by mask_value
  [+] MY_VAR=value              ← cyan [+] prefix, extra via CLAUDEBOX_EXTRA_ENV

Mounts:
  CWD         /path/to/project   (read-write)
  ~/.claude   ~/.claudebox        (read-write)
  credentials ~/.claude/.credentials.json (read-write)   ← only if CREDS_MOUNT=true

Network:
  full (host network)

Implementation rules:

  1. Single loop over all three key arrays in order: sandbox keys first ([~]), then host keys ([>]), then extra keys ([+])
  2. Each line: {COLOR}{PREFIX}{RESET} {var}={masked_value} — the prefix [~], [>], [+] is printed in color AND must be readable without color (accessibility per D-07)
  3. PATH gets the same multiline indented treatment as the current implementation — show label then each path entry indented
  4. After the env list, print a blank line then ${BOLD}Mounts:${RESET} section:
    • Always show: CWD $CWD (read-write)
    • Always show: ~/.claude $HOME/.claudebox (read-write)
    • Conditionally show (when CREDS_MOUNT=true): credentials $HOME/.claude/.credentials.json (read-write)
    • Use consistent column alignment — pad the label to a fixed width (e.g., 12 chars) so values align
  5. After Mounts, print a blank line then ${BOLD}Network:${RESET} section:
    • Always show: full (host network)
    • This is a Phase 4 placeholder — no dynamic logic
  6. All output to stderr (>&2)
  7. Unset allowlisted host vars are silently omitted from the display (discretion decision from CONTEXT.md)

The existing AUDIT_SANDBOX_KEYS, AUDIT_SANDBOX_VALS, AUDIT_HOST_KEYS, AUDIT_HOST_VALS, AUDIT_EXTRA_KEYS, AUDIT_EXTRA_VALS arrays must NOT be restructured — only print_audit changes.

<acceptance_criteria>

  • grep '\[~\]' claudebox.sh matches inside print_audit, used for sandbox-generated vars
  • grep '\[>\]' claudebox.sh matches inside print_audit, used for host-allowlisted vars
  • grep '\[+\]' claudebox.sh matches inside print_audit, used for extra vars
  • grep 'Mounts:' claudebox.sh matches inside print_audit
  • grep 'Network:' claudebox.sh matches inside print_audit
  • grep 'full (host network)' claudebox.sh matches inside print_audit
  • grep 'CREDS_MOUNT' claudebox.sh shows the Mounts section conditionally includes the credentials line
  • grep 'mask_value' claudebox.sh shows mask_value is called for every env var value in the new loops (not just some vars)
  • grep '>&2' claudebox.sh — all print_audit echo/printf calls redirect to stderr
  • bash -n claudebox.sh exits 0 (no syntax errors) </acceptance_criteria>

Verification

# Syntax check
bash -n claudebox.sh

# Verify credential bind present in both exec and dry-run blocks
grep -n 'credentials.json' claudebox.sh

# Verify read-write (not ro-bind) for credentials
grep 'ro-bind.*credentials' claudebox.sh  # must return nothing

# Verify prefix scheme present
grep -E '\[~\]|\[>\]|\[+\]' claudebox.sh

# Verify Mounts and Network sections
grep -E 'Mounts:|Network:|full .host network.' claudebox.sh

# Functional test: dry-run with credentials file present
# (Create a test file, run dry-run, confirm bind appears in output)
touch /tmp/test_credentials.json
HOME_ORIG="$HOME"
# If test harness available: claudebox --dry-run should show --bind ...credentials.json

Must-Haves

  • ~/.claude/.credentials.json is mounted read-write into the sandbox when it exists on the host
  • When the file does not exist, claudebox starts without any error or warning
  • The exec bwrap call and the --dry-run display block are in sync (credential bind appears in both or neither)
  • The audit screen shows [~], [>], and [+] prefixes that distinguish env var categories without relying on color alone
  • The audit screen shows a Mounts section listing CWD, ~/.claude, and (conditionally) the credentials file
  • The audit screen shows a Network section with "full (host network)" as a Phase 6 placeholder
  • bash -n claudebox.sh passes (no syntax errors introduced)

Output

After completion, create .planning/phases/04-auth-passthrough/04-01-SUMMARY.md