11 KiB
| plan | phase | wave | depends_on | files_modified | requirements | autonomous | must_haves | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 04 | 1 |
|
|
true |
|
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 subscriptionANTHROPIC_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.jsonback to the host
Attack vectors:
- Sandbox code reads
.credentials.jsonand exfiltrates tokens over the network (full host network in Phase 4) - Sandbox code overwrites
.credentials.jsonwith 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~/.claudeon the host
Mitigations:
- Bind is file-scoped (
--bind .credentials.json .credentials.json), NOT directory-scoped — sandbox cannot create new files in the host~/.claudedirectory via this mount - Read-write is required for legitimate OAuth refresh; this is an accepted trade-off documented in D-02
ANTHROPIC_API_KEYis masked in audit output bymask_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 284–350 — the --dry-run display block and exec bwrap call; credential bind goes in both places
- claudebox.sh lines 98–103 — 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:
- Sets
CREDS_FILE="$HOME/.claude/.credentials.json" - Checks
if [[ -f "$CREDS_FILE" ]]; thensetsCREDS_MOUNT=true; elseCREDS_MOUNT=false - Adds
--bind "$CREDS_FILE" "$HOME/.claude/.credentials.json"to the exec bwrap call (after the--symlink "$HOME/.claudebox" "$HOME/.claude"line) whenCREDS_MOUNT=true - Mirrors the same conditional bind in the
--dry-rundisplay 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.shshows the variable declared asCREDS_FILE="$HOME/.claude/.credentials.json"grep -n 'CREDS_MOUNT' claudebox.shshows both the conditional set and usage in bwrap execgrep -n 'credentials.json' claudebox.shshows 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.shreturns nothing grep -A2 'CREDS_MOUNT' claudebox.shshows the condition is[[ -f "$CREDS_FILE" ]]— file existence check, not variable existence- No
echoor>&2output is produced when the credentials file is absent — the skip is completely silent </acceptance_criteria>
<read_first>
- claudebox.sh lines 171–263 — 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 65–76 — ANSI color variable definitions (BOLD, RESET, DIM, CYAN, YELLOW, GREEN, RED already defined)
- claudebox.sh lines 78–91 — 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 227–263) 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:
- Single loop over all three key arrays in order: sandbox keys first ([~]), then host keys ([>]), then extra keys ([+])
- Each line:
{COLOR}{PREFIX}{RESET} {var}={masked_value}— the prefix[~],[>],[+]is printed in color AND must be readable without color (accessibility per D-07) - PATH gets the same multiline indented treatment as the current implementation — show label then each path entry indented
- 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
- Always show:
- 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
- Always show:
- All output to stderr (
>&2) - 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.shmatches inside print_audit, used for sandbox-generated varsgrep '\[>\]' claudebox.shmatches inside print_audit, used for host-allowlisted varsgrep '\[+\]' claudebox.shmatches inside print_audit, used for extra varsgrep 'Mounts:' claudebox.shmatches inside print_auditgrep 'Network:' claudebox.shmatches inside print_auditgrep 'full (host network)' claudebox.shmatches inside print_auditgrep 'CREDS_MOUNT' claudebox.shshows the Mounts section conditionally includes the credentials linegrep 'mask_value' claudebox.shshows 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 stderrbash -n claudebox.shexits 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.jsonis 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.shpasses (no syntax errors introduced)
Output
After completion, create .planning/phases/04-auth-passthrough/04-01-SUMMARY.md