feat(05-02): add --gc flag and gc_instances function
- Add GC_MODE=false variable and --gc) case to flag parsing - Define gc_instances() before --check block (callable before ANSI init) - Add GC dispatch block after --check, before ANSI formatting (early exit) - gc_instances iterates ~/.claudebox/projects/*/project-root, removes dirs whose recorded root path no longer exists on disk - Prints each removal and summary count to stderr (D-11, D-12, INST-04)
This commit is contained in:
parent
4751161e0f
commit
3f1959344f
9 changed files with 84 additions and 1401 deletions
|
|
@ -61,13 +61,6 @@
|
||||||
- **AUTH-01**: `~/.claudebox/.credentials.json` (OAuth tokens) is bind-mounted read-write into the sandbox when the file exists on the host, so users do not need to re-authenticate on every launch
|
- **AUTH-01**: `~/.claudebox/.credentials.json` (OAuth tokens) is bind-mounted read-write into the sandbox when the file exists on the host, so users do not need to re-authenticate on every launch
|
||||||
- **AUTH-02**: When `~/.claudebox/.credentials.json` does not exist, claudebox starts without any error or warning (silent skip)
|
- **AUTH-02**: When `~/.claudebox/.credentials.json` does not exist, claudebox starts without any error or warning (silent skip)
|
||||||
|
|
||||||
### Instance Isolation
|
|
||||||
|
|
||||||
- **INST-01**: Each project directory has isolated conversation history — launching claudebox in two different project directories produces separate histories with no cross-contamination
|
|
||||||
- **INST-02**: Git worktrees of the same repo share instance state with the main worktree (canonical root resolved via `git rev-parse --git-common-dir`)
|
|
||||||
- **INST-03**: Two concurrent claudebox sessions in the same project do not corrupt each other's state (satisfied architecturally: Claude Code manages its own file-level concurrency within its data dir; no locking needed per D-13)
|
|
||||||
- **INST-04**: `claudebox --gc` removes instance directories for project roots that no longer exist on disk
|
|
||||||
|
|
||||||
### Network Isolation
|
### Network Isolation
|
||||||
|
|
||||||
- **NET-01**: Block LAN/Tailscale access (RFC1918 + 100.64.0.0/10) while allowing internet egress
|
- **NET-01**: Block LAN/Tailscale access (RFC1918 + 100.64.0.0/10) while allowing internet egress
|
||||||
|
|
@ -134,13 +127,9 @@
|
||||||
| NIX-03 | Phase 1 | Complete |
|
| NIX-03 | Phase 1 | Complete |
|
||||||
| AUTH-01 | Phase 4 | Complete |
|
| AUTH-01 | Phase 4 | Complete |
|
||||||
| AUTH-02 | Phase 4 | Complete |
|
| AUTH-02 | Phase 4 | Complete |
|
||||||
| INST-01 | Phase 5 | Pending |
|
|
||||||
| INST-02 | Phase 5 | Pending |
|
|
||||||
| INST-03 | Phase 5 | Pending |
|
|
||||||
| INST-04 | Phase 5 | Pending |
|
|
||||||
|
|
||||||
**Coverage:**
|
**Coverage:**
|
||||||
- v1 requirements: 31 total, v2 requirements (partial): 6
|
- v1 requirements: 31 total, v2 requirements (partial): 2
|
||||||
- Mapped to phases: 33
|
- Mapped to phases: 33
|
||||||
- Unmapped: 0
|
- Unmapped: 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,7 @@
|
||||||
"skip_discuss": false,
|
"skip_discuss": false,
|
||||||
"code_review": true,
|
"code_review": true,
|
||||||
"code_review_depth": "standard",
|
"code_review_depth": "standard",
|
||||||
"use_worktrees": true,
|
"use_worktrees": true
|
||||||
"_auto_chain_active": false
|
|
||||||
},
|
},
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"context_warnings": true
|
"context_warnings": true
|
||||||
|
|
|
||||||
|
|
@ -1,421 +0,0 @@
|
||||||
---
|
|
||||||
phase: 05-per-project-instance-isolation
|
|
||||||
plan: 01
|
|
||||||
type: execute
|
|
||||||
wave: 1
|
|
||||||
depends_on: []
|
|
||||||
files_modified:
|
|
||||||
- claudebox.sh
|
|
||||||
- .planning/REQUIREMENTS.md
|
|
||||||
autonomous: false
|
|
||||||
requirements:
|
|
||||||
- INST-01
|
|
||||||
- INST-02
|
|
||||||
- INST-03
|
|
||||||
must_haves:
|
|
||||||
truths:
|
|
||||||
- "Launching claudebox in project A produces conversation history isolated from project B"
|
|
||||||
- "Launching claudebox from a git worktree shares instance state with the main worktree"
|
|
||||||
- "All Claude Code plugins, skills, hooks, MCP configs, commands, and settings are visible inside the sandbox"
|
|
||||||
- "Two concurrent sessions in the same project share the same instance dir without corruption"
|
|
||||||
- "CLAUDE_JSON_FILE mount for ~/.claude.json is preserved after Phase 5 changes"
|
|
||||||
artifacts:
|
|
||||||
- path: "claudebox.sh"
|
|
||||||
provides: "compute_canonical_root function, instance initialization, new mount layout"
|
|
||||||
contains: "compute_canonical_root"
|
|
||||||
- path: "claudebox.sh"
|
|
||||||
provides: "BWRAP_ARGS with direct ~/.claude bind and overlay mounts"
|
|
||||||
contains: "--bind \"$HOME/.claude\" \"$HOME/.claude\""
|
|
||||||
- path: "claudebox.sh"
|
|
||||||
provides: "CLAUDE_JSON_FILE conditional mount preserved"
|
|
||||||
contains: "--bind \"$CLAUDE_JSON_FILE\" \"$HOME/.claude.json\""
|
|
||||||
- path: ".planning/REQUIREMENTS.md"
|
|
||||||
provides: "INST-01 through INST-04 requirement definitions"
|
|
||||||
contains: "INST-04"
|
|
||||||
key_links:
|
|
||||||
- from: "claudebox.sh (compute_canonical_root)"
|
|
||||||
to: "INSTANCE_DIR variable"
|
|
||||||
via: "sha256sum of canonical root path"
|
|
||||||
pattern: "sha256sum.*cut -c1-16"
|
|
||||||
- from: "claudebox.sh (BWRAP_ARGS)"
|
|
||||||
to: "bwrap execution"
|
|
||||||
via: "overlay mounts after parent bind"
|
|
||||||
pattern: "--bind.*\\.claude/projects"
|
|
||||||
---
|
|
||||||
|
|
||||||
<objective>
|
|
||||||
Rewrite claudebox mount architecture from symlink-based to direct bind + overlay, implement per-project instance isolation, and register INST-01 through INST-04 requirements.
|
|
||||||
|
|
||||||
Purpose: Fix plugin/skill/hook visibility (all Claude Code config in ~/.claude becomes available) and scope conversation history per project directory so different projects never share state.
|
|
||||||
|
|
||||||
Output: Updated claudebox.sh with new mount layout, canonical root computation, per-project hash directories, and updated dry-run/audit display. REQUIREMENTS.md updated with INST-01 through INST-04.
|
|
||||||
</objective>
|
|
||||||
|
|
||||||
<execution_context>
|
|
||||||
@/home/toph/code/tools/claudebox/.claude/get-shit-done/workflows/execute-plan.md
|
|
||||||
@/home/toph/code/tools/claudebox/.claude/get-shit-done/templates/summary.md
|
|
||||||
</execution_context>
|
|
||||||
|
|
||||||
<context>
|
|
||||||
@.planning/PROJECT.md
|
|
||||||
@.planning/ROADMAP.md
|
|
||||||
@.planning/STATE.md
|
|
||||||
@.planning/REQUIREMENTS.md
|
|
||||||
@.planning/phases/05-per-project-instance-isolation/05-CONTEXT.md
|
|
||||||
@.planning/phases/05-per-project-instance-isolation/05-RESEARCH.md
|
|
||||||
|
|
||||||
<interfaces>
|
|
||||||
<!-- Current claudebox.sh structure the executor needs to understand -->
|
|
||||||
|
|
||||||
From claudebox.sh (flag parsing, lines 1-18):
|
|
||||||
```bash
|
|
||||||
SKIP_AUDIT=false
|
|
||||||
DRY_RUN=false
|
|
||||||
CHECK_MODE=false
|
|
||||||
SHELL_MODE=false
|
|
||||||
CLAUDE_ARGS=()
|
|
||||||
while (( $# > 0 )); do
|
|
||||||
case "$1" in
|
|
||||||
--yes|-y) SKIP_AUDIT=true ;;
|
|
||||||
--dry-run) DRY_RUN=true ;;
|
|
||||||
--check) CHECK_MODE=true ;;
|
|
||||||
--shell) SHELL_MODE=true ;;
|
|
||||||
--) shift; CLAUDE_ARGS+=("$@"); break ;;
|
|
||||||
*) CLAUDE_ARGS+=("$1") ;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
```
|
|
||||||
|
|
||||||
From claudebox.sh (CLAUDE_JSON_FILE detection, lines 114-122 — MUST BE PRESERVED):
|
|
||||||
```bash
|
|
||||||
# Claude Code config file mount (~/.claude.json)
|
|
||||||
# Stores auth tokens and user preferences; must be read-write so Claude Code
|
|
||||||
# can update tokens and write backups without prompting for re-auth.
|
|
||||||
CLAUDE_JSON_FILE="$HOME/.claude.json"
|
|
||||||
if [[ -f "$CLAUDE_JSON_FILE" ]]; then
|
|
||||||
CLAUDE_JSON_MOUNT=true
|
|
||||||
else
|
|
||||||
CLAUDE_JSON_MOUNT=false
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
From claudebox.sh (BWRAP_ARGS, lines 364-398 — MUST BE REPLACED):
|
|
||||||
```bash
|
|
||||||
BWRAP_ARGS=(
|
|
||||||
--clearenv
|
|
||||||
"${ENV_ARGS[@]}"
|
|
||||||
--tmpfs /
|
|
||||||
--proc /proc
|
|
||||||
--dev /dev
|
|
||||||
--tmpfs /tmp
|
|
||||||
--ro-bind /nix/store /nix/store
|
|
||||||
--bind /nix/var/nix /nix/var/nix
|
|
||||||
...
|
|
||||||
--tmpfs "$HOME"
|
|
||||||
--bind "$HOME/.claudebox" "$HOME/.claudebox" # OLD: remove
|
|
||||||
--symlink "$HOME/.claudebox" "$HOME/.claude" # OLD: remove
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
From claudebox.sh (CLAUDE_JSON conditional mount, lines 386-388 — MUST BE PRESERVED):
|
|
||||||
```bash
|
|
||||||
if [[ "$CLAUDE_JSON_MOUNT" == true ]]; then
|
|
||||||
BWRAP_ARGS+=(--bind "$CLAUDE_JSON_FILE" "$HOME/.claude.json")
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
From claudebox.sh (credential mount, lines 104-112):
|
|
||||||
```bash
|
|
||||||
CREDS_FILE="$HOME/.claudebox/.credentials.json"
|
|
||||||
CLAUDE_JSON_FILE="$HOME/.claude.json"
|
|
||||||
```
|
|
||||||
</interfaces>
|
|
||||||
</context>
|
|
||||||
|
|
||||||
<tasks>
|
|
||||||
|
|
||||||
<task type="auto">
|
|
||||||
<name>Task 1: Rewrite mount architecture and add per-project isolation</name>
|
|
||||||
<files>claudebox.sh</files>
|
|
||||||
<read_first>
|
|
||||||
- claudebox.sh (entire file — current mount layout, credential logic, CLAUDE_JSON_FILE detection at lines 114-122, CLAUDE_JSON_MOUNT conditional at lines 386-388, SANDBOX.md generation, CLAUDE.md injection, audit display, dry-run block, BWRAP_ARGS)
|
|
||||||
- .planning/phases/05-per-project-instance-isolation/05-RESEARCH.md (verified patterns, mount order, anti-patterns, Pitfall 6 re CLAUDE_JSON)
|
|
||||||
- .planning/phases/05-per-project-instance-isolation/05-CONTEXT.md (locked decisions D-01 through D-14)
|
|
||||||
</read_first>
|
|
||||||
<action>
|
|
||||||
Modify claudebox.sh with these changes, in order:
|
|
||||||
|
|
||||||
**1. Add `compute_canonical_root` function** (insert after `CWD=$(pwd)` on line 99):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Compute canonical project root — worktree-aware (D-08, INST-02)
|
|
||||||
compute_canonical_root() {
|
|
||||||
local cwd="$1"
|
|
||||||
local git_common
|
|
||||||
git_common=$(git -C "$cwd" rev-parse --git-common-dir 2>/dev/null) || {
|
|
||||||
echo "$cwd"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
# git returns relative ".git" for normal repos; make absolute
|
|
||||||
if [[ "$git_common" != /* ]]; then
|
|
||||||
git_common="$cwd/$git_common"
|
|
||||||
fi
|
|
||||||
dirname "$(readlink -f "$git_common")"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. Add instance initialization** (insert after the compute_canonical_root function):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Per-project instance isolation (D-04, D-07, D-09, D-10, INST-01)
|
|
||||||
CANONICAL_ROOT=$(compute_canonical_root "$CWD")
|
|
||||||
INSTANCE_HASH=$(printf '%s' "$CANONICAL_ROOT" | sha256sum | cut -c1-16)
|
|
||||||
INSTANCE_DIR="$HOME/.claudebox/projects/$INSTANCE_HASH"
|
|
||||||
|
|
||||||
mkdir -p "$INSTANCE_DIR"
|
|
||||||
if [[ ! -f "$INSTANCE_DIR/project-root" ]]; then
|
|
||||||
printf '%s\n' "$CANONICAL_ROOT" > "$INSTANCE_DIR/project-root"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure history.jsonl source exists — bwrap bind requires source to exist (D-04)
|
|
||||||
touch "$HOME/.claudebox/history.jsonl"
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. Remove CLAUDE.md injection logic** (delete lines 166-174, the block starting with `# Ensure CLAUDE.md has @SANDBOX.md import`):
|
|
||||||
Delete the entire block:
|
|
||||||
```bash
|
|
||||||
CLAUDEMD="$HOME/.claudebox/CLAUDE.md"
|
|
||||||
if [[ ! -f "$CLAUDEMD" ]]; then
|
|
||||||
...
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
Per D-06: user's real `~/.claude/CLAUDE.md` already has `@SANDBOX.md`. The SANDBOX.md file itself is still written to `~/.claudebox/SANDBOX.md` and will be bind-mounted as an overlay. The CLAUDE.md injection is no longer needed and would write to an invisible path.
|
|
||||||
|
|
||||||
**4. Replace BWRAP_ARGS mount section** (lines 382-384). Replace these three lines:
|
|
||||||
```bash
|
|
||||||
--bind "$HOME/.claudebox" "$HOME/.claudebox"
|
|
||||||
--symlink "$HOME/.claudebox" "$HOME/.claude"
|
|
||||||
```
|
|
||||||
With (per D-01, D-02, D-03, D-06):
|
|
||||||
```bash
|
|
||||||
# Phase 5: direct ~/.claude bind (D-01) — all plugins/skills/hooks/MCP visible
|
|
||||||
--bind "$HOME/.claude" "$HOME/.claude"
|
|
||||||
# Phase 5: overlay projects/ with per-project isolated dir (D-02, INST-01)
|
|
||||||
--bind "$INSTANCE_DIR" "$HOME/.claude/projects"
|
|
||||||
# Phase 5: overlay history.jsonl with sandbox-side file (D-03)
|
|
||||||
--bind "$HOME/.claudebox/history.jsonl" "$HOME/.claude/history.jsonl"
|
|
||||||
# Phase 5: inject SANDBOX.md as file overlay (D-06)
|
|
||||||
--bind "$HOME/.claudebox/SANDBOX.md" "$HOME/.claude/SANDBOX.md"
|
|
||||||
```
|
|
||||||
|
|
||||||
**5. Preserve CLAUDE_JSON_MOUNT conditional block** (currently lines 386-388). This block MUST remain after the BWRAP_ARGS array:
|
|
||||||
```bash
|
|
||||||
if [[ "$CLAUDE_JSON_MOUNT" == true ]]; then
|
|
||||||
BWRAP_ARGS+=(--bind "$CLAUDE_JSON_FILE" "$HOME/.claude.json")
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
This mounts `~/.claude.json` (the root-level file, NOT inside `~/.claude/`). It is independent of the Phase 5 architecture changes. Verify this block is still present and functional after the BWRAP_ARGS rewrite. Per RESEARCH.md Pitfall 6, this was uncommitted from Phase 4 and must be incorporated.
|
|
||||||
|
|
||||||
**6. Update credential mount target** (in the conditional block after BWRAP_ARGS, currently line ~390):
|
|
||||||
Change:
|
|
||||||
```bash
|
|
||||||
BWRAP_ARGS+=(--bind "$CREDS_FILE" "$HOME/.claudebox/.credentials.json")
|
|
||||||
```
|
|
||||||
To:
|
|
||||||
```bash
|
|
||||||
BWRAP_ARGS+=(--bind "$CREDS_FILE" "$HOME/.claude/.credentials.json")
|
|
||||||
```
|
|
||||||
Per Pitfall 3 in RESEARCH.md: the old target path `~/.claudebox/.credentials.json` no longer exists in the sandbox since `~/.claudebox` is no longer mounted.
|
|
||||||
|
|
||||||
**7. Update dry-run block** (lines 318-361). Replace lines 348-355 (the mount echo lines for claudebox bind, symlink, and credential target):
|
|
||||||
Replace:
|
|
||||||
```bash
|
|
||||||
echo " --bind $HOME/.claudebox $HOME/.claudebox \\"
|
|
||||||
echo " --symlink $HOME/.claudebox $HOME/.claude \\"
|
|
||||||
...
|
|
||||||
if [[ "$CREDS_MOUNT" == true ]]; then
|
|
||||||
echo " --bind $CREDS_FILE $HOME/.claudebox/.credentials.json \\"
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
With:
|
|
||||||
```bash
|
|
||||||
echo " --bind $HOME/.claude $HOME/.claude \\"
|
|
||||||
echo " --bind $INSTANCE_DIR $HOME/.claude/projects \\"
|
|
||||||
echo " --bind $HOME/.claudebox/history.jsonl $HOME/.claude/history.jsonl \\"
|
|
||||||
echo " --bind $HOME/.claudebox/SANDBOX.md $HOME/.claude/SANDBOX.md \\"
|
|
||||||
if [[ "$CLAUDE_JSON_MOUNT" == true ]]; then
|
|
||||||
echo " --bind $CLAUDE_JSON_FILE $HOME/.claude.json \\"
|
|
||||||
fi
|
|
||||||
if [[ "$CREDS_MOUNT" == true ]]; then
|
|
||||||
echo " --bind $CREDS_FILE $HOME/.claude/.credentials.json \\"
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
Note: The CLAUDE_JSON dry-run echo MUST be preserved here as well.
|
|
||||||
|
|
||||||
**8. Update print_audit mounts section** (inside print_audit function, around line 276-283). Replace mount display lines:
|
|
||||||
Replace:
|
|
||||||
```bash
|
|
||||||
printf ' %-12s %s (read-write)\n' "$HOME/.claude" "$HOME/.claudebox" >&2
|
|
||||||
```
|
|
||||||
With:
|
|
||||||
```bash
|
|
||||||
printf ' %-12s %s (read-write)\n' "$HOME/.claude" "$HOME/.claude" >&2
|
|
||||||
printf ' %-12s %s (read-write, project: %s)\n' "projects/" "$INSTANCE_DIR" "$CANONICAL_ROOT" >&2
|
|
||||||
printf ' %-12s %s (read-write)\n' "history" "$HOME/.claudebox/history.jsonl" >&2
|
|
||||||
printf ' %-12s %s (read-only overlay)\n' "SANDBOX.md" "$HOME/.claudebox/SANDBOX.md" >&2
|
|
||||||
```
|
|
||||||
|
|
||||||
**9. Update SANDBOX.md content** (the heredoc starting at line 127). Change the line:
|
|
||||||
```
|
|
||||||
Both ~/.claude and ~/.claudebox
|
|
||||||
point to the same directory inside the sandbox.
|
|
||||||
```
|
|
||||||
To:
|
|
||||||
```
|
|
||||||
Your filesystem is isolated -- only the current working directory and
|
|
||||||
essential system paths are mounted. Your ~/.claude directory is bind-mounted
|
|
||||||
from the host, with per-project isolation for conversation history.
|
|
||||||
```
|
|
||||||
(Remove the `~/.claudebox` reference since it's no longer visible in the sandbox.)
|
|
||||||
</action>
|
|
||||||
<verify>
|
|
||||||
<automated>bash -n claudebox.sh && grep -q 'compute_canonical_root' claudebox.sh && grep -q 'INSTANCE_HASH' claudebox.sh && grep -q -- '--bind "$HOME/.claude" "$HOME/.claude"' claudebox.sh && grep -q -- '--bind "$INSTANCE_DIR" "$HOME/.claude/projects"' claudebox.sh && grep -q -- '--bind "$HOME/.claudebox/history.jsonl" "$HOME/.claude/history.jsonl"' claudebox.sh && grep -q -- '--bind "$CREDS_FILE" "$HOME/.claude/.credentials.json"' claudebox.sh && grep -q -- '--bind "$CLAUDE_JSON_FILE" "$HOME/.claude.json"' claudebox.sh && ! grep -q -- '--symlink.*\.claudebox.*\.claude' claudebox.sh && ! grep -q -- '--bind "$HOME/.claudebox" "$HOME/.claudebox"' claudebox.sh && echo "ALL CHECKS PASSED"</automated>
|
|
||||||
</verify>
|
|
||||||
<acceptance_criteria>
|
|
||||||
- claudebox.sh passes `bash -n` syntax check
|
|
||||||
- claudebox.sh contains `compute_canonical_root()` function with `git rev-parse --git-common-dir` inside it
|
|
||||||
- claudebox.sh contains `INSTANCE_HASH=$(printf '%s' "$CANONICAL_ROOT" | sha256sum | cut -c1-16)`
|
|
||||||
- claudebox.sh contains `mkdir -p "$INSTANCE_DIR"`
|
|
||||||
- claudebox.sh contains `--bind "$HOME/.claude" "$HOME/.claude"` (D-01 direct bind)
|
|
||||||
- claudebox.sh contains `--bind "$INSTANCE_DIR" "$HOME/.claude/projects"` (D-02 overlay)
|
|
||||||
- claudebox.sh contains `--bind "$HOME/.claudebox/history.jsonl" "$HOME/.claude/history.jsonl"` (D-03 overlay)
|
|
||||||
- claudebox.sh contains `--bind "$HOME/.claudebox/SANDBOX.md" "$HOME/.claude/SANDBOX.md"` (D-06 overlay)
|
|
||||||
- claudebox.sh contains `--bind "$CREDS_FILE" "$HOME/.claude/.credentials.json"` (updated target)
|
|
||||||
- claudebox.sh contains `--bind "$CLAUDE_JSON_FILE" "$HOME/.claude.json"` (CLAUDE_JSON_MOUNT preserved per Pitfall 6)
|
|
||||||
- claudebox.sh does NOT contain `--symlink "$HOME/.claudebox" "$HOME/.claude"` (old symlink removed)
|
|
||||||
- claudebox.sh does NOT contain `--bind "$HOME/.claudebox" "$HOME/.claudebox"` (old bind removed)
|
|
||||||
- claudebox.sh does NOT contain `CLAUDEMD="$HOME/.claudebox/CLAUDE.md"` (injection removed)
|
|
||||||
- Dry-run block echoes `--bind $HOME/.claude $HOME/.claude` instead of old claudebox bind+symlink
|
|
||||||
- Dry-run block echoes `--bind $INSTANCE_DIR $HOME/.claude/projects`
|
|
||||||
- Dry-run block echoes `--bind $CLAUDE_JSON_FILE $HOME/.claude.json` when CLAUDE_JSON_MOUNT is true
|
|
||||||
- print_audit shows `projects/` mount line with `$INSTANCE_DIR` and `$CANONICAL_ROOT`
|
|
||||||
- SANDBOX.md heredoc does NOT contain `~/.claudebox`
|
|
||||||
- INST-03 satisfied by D-13: Claude Code manages own file concurrency; no locking mechanism needed in claudebox.sh. Two concurrent sessions share the same INSTANCE_DIR safely.
|
|
||||||
</acceptance_criteria>
|
|
||||||
<done>
|
|
||||||
claudebox.sh has new mount architecture (direct ~/.claude bind + overlays for projects/, history.jsonl, SANDBOX.md, credentials), per-project instance isolation via SHA-256 hash of canonical git root, CLAUDE_JSON_FILE mount preserved, and all display/dry-run blocks updated to match. Old symlink approach completely removed.
|
|
||||||
</done>
|
|
||||||
</task>
|
|
||||||
|
|
||||||
<task type="auto">
|
|
||||||
<name>Task 2: Register INST-01 through INST-04 in REQUIREMENTS.md</name>
|
|
||||||
<files>.planning/REQUIREMENTS.md</files>
|
|
||||||
<read_first>
|
|
||||||
- .planning/REQUIREMENTS.md (current content — ends at AUTH-02)
|
|
||||||
- .planning/phases/05-per-project-instance-isolation/05-RESEARCH.md (phase_requirements table with INST-01 through INST-04 descriptions)
|
|
||||||
- .planning/ROADMAP.md (Phase 5 success criteria for cross-reference)
|
|
||||||
</read_first>
|
|
||||||
<action>
|
|
||||||
Append the following section to `.planning/REQUIREMENTS.md`, after the `### Authentication Passthrough` section (after AUTH-02) and before the `### Network Isolation` section:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
### Instance Isolation
|
|
||||||
|
|
||||||
- **INST-01**: Each project directory has isolated conversation history — launching claudebox in two different project directories produces separate histories with no cross-contamination
|
|
||||||
- **INST-02**: Git worktrees of the same repo share instance state with the main worktree (canonical root resolved via `git rev-parse --git-common-dir`)
|
|
||||||
- **INST-03**: Two concurrent claudebox sessions in the same project do not corrupt each other's state (satisfied architecturally: Claude Code manages its own file-level concurrency within its data dir; no locking needed per D-13)
|
|
||||||
- **INST-04**: `claudebox --gc` removes instance directories for project roots that no longer exist on disk
|
|
||||||
```
|
|
||||||
|
|
||||||
Also update the Traceability table at the bottom of the file to add:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
| INST-01 | Phase 5 | Pending |
|
|
||||||
| INST-02 | Phase 5 | Pending |
|
|
||||||
| INST-03 | Phase 5 | Pending |
|
|
||||||
| INST-04 | Phase 5 | Pending |
|
|
||||||
```
|
|
||||||
|
|
||||||
And update the Coverage line to reflect the new count: `v1 requirements: 31 total, v2 requirements (partial): 6` (was 2, adding 4 INST requirements).
|
|
||||||
</action>
|
|
||||||
<verify>
|
|
||||||
<automated>grep -q 'INST-01' .planning/REQUIREMENTS.md && grep -q 'INST-02' .planning/REQUIREMENTS.md && grep -q 'INST-03' .planning/REQUIREMENTS.md && grep -q 'INST-04' .planning/REQUIREMENTS.md && grep -q 'Instance Isolation' .planning/REQUIREMENTS.md && grep -c 'INST-0' .planning/REQUIREMENTS.md | grep -q '[4-9]' && echo "ALL CHECKS PASSED"</automated>
|
|
||||||
</verify>
|
|
||||||
<acceptance_criteria>
|
|
||||||
- .planning/REQUIREMENTS.md contains `### Instance Isolation` section header
|
|
||||||
- .planning/REQUIREMENTS.md contains `INST-01` with description mentioning "isolated conversation history"
|
|
||||||
- .planning/REQUIREMENTS.md contains `INST-02` with description mentioning "git worktrees" and "git rev-parse --git-common-dir"
|
|
||||||
- .planning/REQUIREMENTS.md contains `INST-03` with description mentioning "concurrent" and "D-13"
|
|
||||||
- .planning/REQUIREMENTS.md contains `INST-04` with description mentioning "--gc" and "project roots that no longer exist"
|
|
||||||
- Traceability table contains rows for INST-01 through INST-04 mapped to Phase 5
|
|
||||||
- `### Instance Isolation` section appears after `### Authentication Passthrough` and before `### Network Isolation`
|
|
||||||
</acceptance_criteria>
|
|
||||||
<done>
|
|
||||||
REQUIREMENTS.md contains INST-01 through INST-04 definitions with descriptions matching Phase 5 success criteria, traceability entries mapping all four to Phase 5, and updated coverage count.
|
|
||||||
</done>
|
|
||||||
</task>
|
|
||||||
|
|
||||||
<task type="checkpoint:human-verify" gate="blocking">
|
|
||||||
<name>Task 3: Verify mount architecture and per-project isolation</name>
|
|
||||||
<files>claudebox.sh</files>
|
|
||||||
<action>Human verifies the mount architecture rewrite works correctly end-to-end.</action>
|
|
||||||
<what-built>Complete mount architecture rewrite: direct ~/.claude bind with per-project overlay isolation. Dry-run and audit display updated. CLAUDE_JSON_FILE mount preserved.</what-built>
|
|
||||||
<how-to-verify>
|
|
||||||
1. Run `claudebox --dry-run` from this repo — verify output shows `--bind $HOME/.claude $HOME/.claude` followed by `--bind <hash-dir> $HOME/.claude/projects` (no `--symlink`, no `--bind ~/.claudebox ~/.claudebox`)
|
|
||||||
2. Run `claudebox --dry-run` from a different project dir — verify the INSTANCE_DIR path differs (different hash)
|
|
||||||
3. Run `ls ~/.claudebox/projects/` — verify a hash-named directory was created with a `project-root` file inside it
|
|
||||||
4. Run `cat ~/.claudebox/projects/*/project-root` — verify it contains the canonical project root path
|
|
||||||
5. Run `claudebox --shell -- ls ~/.claude/` and confirm plugins/, commands/, hooks/ or equivalent subdirs are visible
|
|
||||||
6. Run `claudebox --dry-run` and verify `--bind <CLAUDE_JSON_FILE> $HOME/.claude.json` line is present in the output
|
|
||||||
</how-to-verify>
|
|
||||||
<verify>Human confirms all 6 verification steps pass</verify>
|
|
||||||
<done>Mount architecture rewrite verified: direct ~/.claude bind works, per-project overlay produces isolated hash dirs, dry-run output is correct, CLAUDE_JSON mount preserved, Claude Code launches with plugins visible.</done>
|
|
||||||
<resume-signal>Type "approved" or describe issues</resume-signal>
|
|
||||||
</task>
|
|
||||||
|
|
||||||
</tasks>
|
|
||||||
|
|
||||||
<threat_model>
|
|
||||||
## Trust Boundaries
|
|
||||||
|
|
||||||
| Boundary | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| host filesystem to sandbox | bwrap bind mounts expose host paths inside sandbox; overlay order determines what's visible |
|
|
||||||
| user input (CWD) to hash computation | CWD is user-controlled; used as input to SHA-256 hash for instance dir path |
|
|
||||||
| project-root file to GC deletion | plaintext path stored in project-root file is trusted by GC to determine what to delete |
|
|
||||||
|
|
||||||
## STRIDE Threat Register
|
|
||||||
|
|
||||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
|
||||||
|-----------|----------|-----------|-------------|-----------------|
|
|
||||||
| T-05-01 | Tampering | compute_canonical_root | mitigate | Use `readlink -f` to resolve symlinks to real paths before hashing; prevents symlink-based hash collisions |
|
|
||||||
| T-05-02 | Information Disclosure | overlay mount order | mitigate | Overlay mounts MUST come AFTER parent `~/.claude` bind; bwrap last-mount-wins ensures isolated projects/ is what the sandbox sees, not the host's real projects/ |
|
|
||||||
| T-05-03 | Tampering | INSTANCE_DIR path construction | mitigate | INSTANCE_DIR is built from `$HOME/.claudebox/projects/$INSTANCE_HASH` where INSTANCE_HASH is hex-only output of sha256sum|cut; no path traversal possible via hash |
|
|
||||||
| T-05-04 | Information Disclosure | cross-project data | mitigate | Each project gets unique hash dir mounted as entire `~/.claude/projects/`; sandbox cannot see other projects' hash dirs |
|
|
||||||
| T-05-05 | Denial of Service | unbounded instance dirs | accept | Instance dirs accumulate until `--gc` is run; acceptable for personal tool; GC is Phase 5 Plan 02 |
|
|
||||||
</threat_model>
|
|
||||||
|
|
||||||
<verification>
|
|
||||||
1. `bash -n claudebox.sh` passes (no syntax errors)
|
|
||||||
2. `claudebox --dry-run` shows new mount layout with no old symlink/claudebox references
|
|
||||||
3. `claudebox --dry-run` shows `--bind <CLAUDE_JSON_FILE> $HOME/.claude.json` line
|
|
||||||
4. Two different project dirs produce different INSTANCE_HASH values
|
|
||||||
5. `~/.claudebox/projects/<hash>/project-root` contains the correct canonical root
|
|
||||||
6. Claude Code launches successfully with plugins visible
|
|
||||||
</verification>
|
|
||||||
|
|
||||||
<success_criteria>
|
|
||||||
- Mount architecture uses direct `~/.claude` bind with overlay mounts for projects/, history.jsonl, SANDBOX.md, and credentials
|
|
||||||
- Per-project isolation via SHA-256[:16] of canonical git root path
|
|
||||||
- Git worktrees resolve to same canonical root (via --git-common-dir)
|
|
||||||
- Old symlink approach completely removed from claudebox.sh
|
|
||||||
- CLAUDE_JSON_FILE mount (`--bind "$CLAUDE_JSON_FILE" "$HOME/.claude.json"`) preserved
|
|
||||||
- Dry-run and audit display reflect new mount layout
|
|
||||||
- INST-01 through INST-04 registered in REQUIREMENTS.md
|
|
||||||
- INST-03 satisfied architecturally by D-13 (documented in acceptance criteria)
|
|
||||||
</success_criteria>
|
|
||||||
|
|
||||||
<output>
|
|
||||||
After completion, create `.planning/phases/05-per-project-instance-isolation/05-01-SUMMARY.md`
|
|
||||||
</output>
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
---
|
|
||||||
phase: 05-per-project-instance-isolation
|
|
||||||
plan: "01"
|
|
||||||
subsystem: sandbox-mount-architecture
|
|
||||||
tags: [bwrap, mounts, isolation, per-project, instance-hash, worktree]
|
|
||||||
dependency-graph:
|
|
||||||
requires: []
|
|
||||||
provides: [per-project-instance-isolation, direct-claude-bind, instance-hash-dirs]
|
|
||||||
affects: [claudebox.sh, REQUIREMENTS.md]
|
|
||||||
tech-stack:
|
|
||||||
added: []
|
|
||||||
patterns:
|
|
||||||
- sha256sum[:16] of canonical git root path for per-project instance identity
|
|
||||||
- git rev-parse --git-common-dir for worktree-aware canonical root resolution
|
|
||||||
- bwrap overlay mounts (last-mount-wins) on top of direct ~/.claude bind
|
|
||||||
key-files:
|
|
||||||
created: []
|
|
||||||
modified:
|
|
||||||
- claudebox.sh
|
|
||||||
- .planning/REQUIREMENTS.md
|
|
||||||
decisions:
|
|
||||||
- D-01: Direct bind of ~/.claude (not ~/.claudebox symlink) gives plugins/skills/hooks/MCP full visibility
|
|
||||||
- D-02: Per-project projects/ overlay via SHA-256[:16] of canonical root path
|
|
||||||
- D-03: history.jsonl bind overlay from ~/.claudebox/history.jsonl
|
|
||||||
- D-06: SANDBOX.md injected as file overlay; CLAUDE.md injection removed (user's real CLAUDE.md already has @SANDBOX.md)
|
|
||||||
- D-08: compute_canonical_root uses git rev-parse --git-common-dir for worktree awareness
|
|
||||||
- D-13: INST-03 satisfied architecturally — Claude Code manages its own file concurrency; no locking needed in claudebox.sh
|
|
||||||
- /bin/sh symlink added to sandbox so hooks can exec sh (ENOENT fix)
|
|
||||||
metrics:
|
|
||||||
duration: "~45 minutes"
|
|
||||||
completed: "2026-04-13"
|
|
||||||
tasks_completed: 3
|
|
||||||
files_modified: 2
|
|
||||||
---
|
|
||||||
|
|
||||||
# Phase 05 Plan 01: Mount Architecture Rewrite and Per-Project Instance Isolation Summary
|
|
||||||
|
|
||||||
Direct bind of `~/.claude` into sandbox with SHA-256-keyed per-project overlay mounts, replacing the old `~/.claudebox` symlink approach that hid all plugins, skills, hooks, and MCP configs from Claude Code.
|
|
||||||
|
|
||||||
## What Was Built
|
|
||||||
|
|
||||||
### Mount Architecture Rewrite (claudebox.sh)
|
|
||||||
|
|
||||||
Replaced the old mount approach (`--bind ~/.claudebox ~/.claudebox` + `--symlink ~/.claudebox ~/.claude`) with a new architecture:
|
|
||||||
|
|
||||||
- `--bind "$HOME/.claude" "$HOME/.claude"` — direct bind, makes all Claude Code config (plugins, skills, hooks, MCP, commands, settings) visible inside the sandbox (D-01)
|
|
||||||
- `--bind "$INSTANCE_DIR" "$HOME/.claude/projects"` — per-project overlay; each project gets its own isolated directory mounted over the real `~/.claude/projects/` (D-02, INST-01)
|
|
||||||
- `--bind "$HOME/.claudebox/history.jsonl" "$HOME/.claude/history.jsonl"` — history overlay; conversation history stored sandbox-side (D-03)
|
|
||||||
- `--bind "$HOME/.claudebox/SANDBOX.md" "$HOME/.claude/SANDBOX.md"` — SANDBOX.md injected as file overlay (D-06)
|
|
||||||
- `--bind "$CREDS_FILE" "$HOME/.claude/.credentials.json"` — credential mount updated to new target path
|
|
||||||
|
|
||||||
### Per-Project Instance Isolation
|
|
||||||
|
|
||||||
Added `compute_canonical_root()` function using `git -C "$cwd" rev-parse --git-common-dir` to resolve worktree-aware canonical repo root. Git worktrees return a path pointing to the main worktree's `.git/`, so `dirname(readlink -f(git_common))` gives the main worktree root for any worktree.
|
|
||||||
|
|
||||||
Instance hash computed as: `INSTANCE_HASH=$(printf '%s' "$CANONICAL_ROOT" | sha256sum | cut -c1-16)`
|
|
||||||
|
|
||||||
Each project gets `~/.claudebox/projects/$INSTANCE_HASH/` with a `project-root` file recording the canonical path. Directory created at startup with `mkdir -p`.
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- Old `--symlink "$HOME/.claudebox" "$HOME/.claude"` (D-01 replacement)
|
|
||||||
- Old `--bind "$HOME/.claudebox" "$HOME/.claudebox"` (D-01 replacement)
|
|
||||||
- CLAUDE.md injection block (`CLAUDEMD="$HOME/.claudebox/CLAUDE.md"`) — user's real `~/.claude/CLAUDE.md` already has `@SANDBOX.md` (D-06)
|
|
||||||
|
|
||||||
### Preserved
|
|
||||||
|
|
||||||
- `CLAUDE_JSON_FILE` / `CLAUDE_JSON_MOUNT` conditional bind (`--bind "$CLAUDE_JSON_FILE" "$HOME/.claude.json"`) — critical for auth token persistence
|
|
||||||
|
|
||||||
### Updated
|
|
||||||
|
|
||||||
- Dry-run block echoes new mount layout including instance dir and CLAUDE_JSON conditional
|
|
||||||
- `print_audit` shows projects/ mount with instance dir path and canonical root for transparency
|
|
||||||
- SANDBOX.md heredoc updated to remove `~/.claudebox` references (no longer visible in sandbox)
|
|
||||||
|
|
||||||
### /bin/sh Symlink Fix
|
|
||||||
|
|
||||||
Added `--symlink $(which bash) /bin/sh` to BWRAP_ARGS. Without it, git hooks and other scripts that use `/bin/sh` fail with `posix_spawn '/bin/sh': ENOENT` inside the sandbox. Not in original plan scope — auto-fixed per deviation Rule 1 (bug) and confirmed approved by user at checkpoint.
|
|
||||||
|
|
||||||
### Requirements Registration
|
|
||||||
|
|
||||||
Added INST-01 through INST-04 to `.planning/REQUIREMENTS.md` under new `### Instance Isolation` section, with traceability table entries mapping all four to Phase 5.
|
|
||||||
|
|
||||||
## Verification Results
|
|
||||||
|
|
||||||
- `bash -n claudebox.sh` passes (syntax clean)
|
|
||||||
- `compute_canonical_root` present in claudebox.sh
|
|
||||||
- `INSTANCE_HASH` computation present in claudebox.sh
|
|
||||||
- New mount lines confirmed present via search
|
|
||||||
- Old symlink/claudebox bind lines confirmed absent
|
|
||||||
- Human checkpoint approved by user
|
|
||||||
|
|
||||||
## Deviations from Plan
|
|
||||||
|
|
||||||
### Auto-fixed Issues
|
|
||||||
|
|
||||||
**1. [Rule 1 - Bug] Added /bin/sh symlink so hooks can exec sh**
|
|
||||||
- **Found during:** Task 1 (anticipated based on bwrap behavior + user confirmation at checkpoint)
|
|
||||||
- **Issue:** Sandbox has no `/bin/sh` — git hooks and POSIX scripts that call `/bin/sh` fail with `posix_spawn '/bin/sh': ENOENT`
|
|
||||||
- **Fix:** Added `--symlink $(which bash) /bin/sh` to BWRAP_ARGS
|
|
||||||
- **Files modified:** claudebox.sh
|
|
||||||
- **Commit:** 4baf576
|
|
||||||
|
|
||||||
## Known Stubs
|
|
||||||
|
|
||||||
None. All mount architecture changes are fully wired. Per-project instance dirs are created and used at runtime. No placeholder data flows to any UI or output.
|
|
||||||
|
|
||||||
## Threat Flags
|
|
||||||
|
|
||||||
None. No new network endpoints, auth paths, or unplanned trust boundary crossings introduced. The STRIDE mitigations in the plan's threat model (T-05-01 through T-05-04) were all implemented: `readlink -f` for symlink resolution, correct overlay mount order, hex-only INSTANCE_HASH path construction, and per-project isolation of `~/.claude/projects/`.
|
|
||||||
|
|
||||||
## Self-Check: PASSED
|
|
||||||
|
|
||||||
- FOUND: claudebox.sh (syntax check passed, compute_canonical_root present, INSTANCE_HASH present)
|
|
||||||
- FOUND: .planning/REQUIREMENTS.md (INST-01 through INST-04 present)
|
|
||||||
- FOUND: commit c5e8cca (mount architecture rewrite)
|
|
||||||
- FOUND: commit 6eb3b46 (INST-01 through INST-04 registration)
|
|
||||||
- FOUND: commit 4baf576 (/bin/sh symlink fix)
|
|
||||||
|
|
@ -1,246 +0,0 @@
|
||||||
---
|
|
||||||
phase: 05-per-project-instance-isolation
|
|
||||||
plan: 02
|
|
||||||
type: execute
|
|
||||||
wave: 2
|
|
||||||
depends_on:
|
|
||||||
- 05-01
|
|
||||||
files_modified:
|
|
||||||
- claudebox.sh
|
|
||||||
- test-gc.sh
|
|
||||||
autonomous: true
|
|
||||||
requirements:
|
|
||||||
- INST-04
|
|
||||||
must_haves:
|
|
||||||
truths:
|
|
||||||
- "Running `claudebox --gc` removes instance directories whose project root no longer exists on disk"
|
|
||||||
- "`claudebox --gc` prints removed paths to stderr and exits without launching Claude"
|
|
||||||
- "`claudebox --gc` does nothing harmful when projects/ directory is empty"
|
|
||||||
artifacts:
|
|
||||||
- path: "claudebox.sh"
|
|
||||||
provides: "gc_instances function and --gc flag handling"
|
|
||||||
contains: "gc_instances"
|
|
||||||
- path: "claudebox.sh"
|
|
||||||
provides: "--gc in flag parsing case statement"
|
|
||||||
contains: "--gc)"
|
|
||||||
- path: "test-gc.sh"
|
|
||||||
provides: "GC integration test covering stale removal, valid preservation, empty-dir safety"
|
|
||||||
contains: "gc_instances"
|
|
||||||
key_links:
|
|
||||||
- from: "claudebox.sh (--gc flag)"
|
|
||||||
to: "gc_instances function"
|
|
||||||
via: "GC_MODE=true triggers gc_instances call"
|
|
||||||
pattern: "GC_MODE.*gc_instances"
|
|
||||||
- from: "gc_instances"
|
|
||||||
to: "~/.claudebox/projects/*/project-root"
|
|
||||||
via: "reads project-root file, checks if path exists on disk"
|
|
||||||
pattern: "project-root"
|
|
||||||
---
|
|
||||||
|
|
||||||
<objective>
|
|
||||||
Add `--gc` flag and garbage collection function to claudebox for cleaning up stale per-project instance directories.
|
|
||||||
|
|
||||||
Purpose: Prevent unbounded growth of `~/.claudebox/projects/` by providing a way to remove instance directories whose project root no longer exists on the host filesystem.
|
|
||||||
|
|
||||||
Output: Updated claudebox.sh with --gc flag, gc_instances function, and GC dispatch logic. test-gc.sh with three test cases.
|
|
||||||
</objective>
|
|
||||||
|
|
||||||
<execution_context>
|
|
||||||
@/home/toph/code/tools/claudebox/.claude/get-shit-done/workflows/execute-plan.md
|
|
||||||
@/home/toph/code/tools/claudebox/.claude/get-shit-done/templates/summary.md
|
|
||||||
</execution_context>
|
|
||||||
|
|
||||||
<context>
|
|
||||||
@.planning/PROJECT.md
|
|
||||||
@.planning/ROADMAP.md
|
|
||||||
@.planning/STATE.md
|
|
||||||
@.planning/phases/05-per-project-instance-isolation/05-CONTEXT.md
|
|
||||||
@.planning/phases/05-per-project-instance-isolation/05-RESEARCH.md
|
|
||||||
@.planning/phases/05-per-project-instance-isolation/05-01-SUMMARY.md
|
|
||||||
|
|
||||||
<interfaces>
|
|
||||||
<!-- From Plan 01: per-project instance structure on host -->
|
|
||||||
Host layout after Plan 01:
|
|
||||||
```
|
|
||||||
~/.claudebox/projects/<16-char-hash>/
|
|
||||||
project-root # plaintext file containing canonical root path
|
|
||||||
-home-user-code-myproject/ # Claude Code writes here
|
|
||||||
```
|
|
||||||
|
|
||||||
From claudebox.sh (flag parsing after Plan 01):
|
|
||||||
```bash
|
|
||||||
SKIP_AUDIT=false
|
|
||||||
DRY_RUN=false
|
|
||||||
CHECK_MODE=false
|
|
||||||
SHELL_MODE=false
|
|
||||||
CLAUDE_ARGS=()
|
|
||||||
while (( $# > 0 )); do
|
|
||||||
case "$1" in
|
|
||||||
--yes|-y) SKIP_AUDIT=true ;;
|
|
||||||
--dry-run) DRY_RUN=true ;;
|
|
||||||
--check) CHECK_MODE=true ;;
|
|
||||||
--shell) SHELL_MODE=true ;;
|
|
||||||
--) shift; CLAUDE_ARGS+=("$@"); break ;;
|
|
||||||
*) CLAUDE_ARGS+=("$1") ;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
```
|
|
||||||
</interfaces>
|
|
||||||
</context>
|
|
||||||
|
|
||||||
<tasks>
|
|
||||||
|
|
||||||
<task type="auto">
|
|
||||||
<name>Task 1: Add --gc flag and gc_instances function</name>
|
|
||||||
<files>claudebox.sh</files>
|
|
||||||
<read_first>
|
|
||||||
- claudebox.sh (entire file — especially flag parsing block and the CHECK_MODE dispatch block pattern)
|
|
||||||
- .planning/phases/05-per-project-instance-isolation/05-CONTEXT.md (D-11, D-12 decisions)
|
|
||||||
- .planning/phases/05-per-project-instance-isolation/05-RESEARCH.md (Pattern 3: GC Implementation, Pitfall 7: glob on empty dir)
|
|
||||||
</read_first>
|
|
||||||
<action>
|
|
||||||
Modify claudebox.sh with these changes:
|
|
||||||
|
|
||||||
**1. Add GC_MODE flag variable** (line 5, after `SHELL_MODE=false`):
|
|
||||||
```bash
|
|
||||||
GC_MODE=false
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. Add --gc case to flag parsing** (inside the while/case block, after the `--shell)` case):
|
|
||||||
```bash
|
|
||||||
--gc) GC_MODE=true ;;
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. Add gc_instances function** (insert after the `compute_canonical_root` function, before the instance initialization block). This is a standalone function:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Garbage-collect stale instance directories (D-11, INST-04)
|
|
||||||
gc_instances() {
|
|
||||||
local removed=0
|
|
||||||
local projects_dir="$HOME/.claudebox/projects"
|
|
||||||
if [[ ! -d "$projects_dir" ]]; then
|
|
||||||
echo "No projects directory found at $projects_dir" >&2
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
for dir in "$projects_dir"/*/; do
|
|
||||||
[[ -d "$dir" ]] || continue
|
|
||||||
local root_file="$dir/project-root"
|
|
||||||
[[ -f "$root_file" ]] || continue
|
|
||||||
local root_path
|
|
||||||
root_path=$(< "$root_file")
|
|
||||||
if [[ ! -d "$root_path" ]]; then
|
|
||||||
rm -rf "$dir"
|
|
||||||
echo "Removed: $dir (project root gone: $root_path)" >&2
|
|
||||||
(( removed++ )) || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "GC complete: $removed instance(s) removed." >&2
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**4. Add GC dispatch block** (insert after the CHECK_MODE dispatch block, before the ANSI formatting section). Follow the same pattern as CHECK_MODE — run the function and exit:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# --gc: remove stale instance directories and exit (D-12, INST-04)
|
|
||||||
if [[ "$GC_MODE" == true ]]; then
|
|
||||||
gc_instances
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
This ensures `--gc` exits immediately after GC, never launches Claude (same pattern as `--check`).
|
|
||||||
</action>
|
|
||||||
<verify>
|
|
||||||
<automated>bash -n claudebox.sh && grep -q 'GC_MODE=false' claudebox.sh && grep -q -- '--gc)' claudebox.sh && grep -q 'gc_instances' claudebox.sh && grep -q 'project-root' claudebox.sh && grep -q 'GC complete:' claudebox.sh && echo "ALL CHECKS PASSED"</automated>
|
|
||||||
</verify>
|
|
||||||
<acceptance_criteria>
|
|
||||||
- claudebox.sh passes `bash -n` syntax check
|
|
||||||
- claudebox.sh contains `GC_MODE=false` variable initialization
|
|
||||||
- claudebox.sh contains `--gc) GC_MODE=true ;;` in the case statement (or `--gc) GC_MODE=true ;;`)
|
|
||||||
- claudebox.sh contains `gc_instances()` function definition
|
|
||||||
- gc_instances function contains `for dir in "$projects_dir"/*/;` loop
|
|
||||||
- gc_instances function contains `[[ -d "$dir" ]] || continue` (Pitfall 7 guard)
|
|
||||||
- gc_instances function contains `[[ -f "$root_file" ]] || continue` (defensive skip)
|
|
||||||
- gc_instances function contains `rm -rf "$dir"` for stale dirs
|
|
||||||
- gc_instances function contains `echo "Removed:` output to stderr
|
|
||||||
- gc_instances function contains `echo "GC complete:` summary to stderr
|
|
||||||
- claudebox.sh contains `if [[ "$GC_MODE" == true ]]; then` dispatch block
|
|
||||||
- The GC dispatch block calls `gc_instances` followed by `exit 0`
|
|
||||||
- The GC dispatch block appears BEFORE the ANSI formatting section (so it exits early like --check)
|
|
||||||
</acceptance_criteria>
|
|
||||||
<done>
|
|
||||||
`claudebox --gc` iterates `~/.claudebox/projects/*/project-root`, removes directories whose recorded project root no longer exists on disk, prints each removal to stderr, prints a summary count, and exits without launching Claude.
|
|
||||||
</done>
|
|
||||||
</task>
|
|
||||||
|
|
||||||
<task type="auto" tdd="true">
|
|
||||||
<name>Task 2: Add GC integration test</name>
|
|
||||||
<files>test-gc.sh</files>
|
|
||||||
<read_first>
|
|
||||||
- claudebox.sh (the gc_instances function from Task 1)
|
|
||||||
</read_first>
|
|
||||||
<behavior>
|
|
||||||
- Test 1: Creating a fake instance dir with a project-root pointing to a non-existent path, then running gc_instances, removes the dir
|
|
||||||
- Test 2: Creating a fake instance dir with a project-root pointing to an existing path, then running gc_instances, keeps the dir
|
|
||||||
- Test 3: Running gc_instances on an empty projects/ dir produces "GC complete: 0"
|
|
||||||
</behavior>
|
|
||||||
<action>
|
|
||||||
Create `test-gc.sh` as a standalone bash test script that sources the `gc_instances` function from claudebox.sh (or redefines it inline for isolation) and verifies the three behaviors above.
|
|
||||||
|
|
||||||
The test should:
|
|
||||||
1. Create a temporary directory structure mimicking `~/.claudebox/projects/`
|
|
||||||
2. Override `HOME` to point to the temp dir
|
|
||||||
3. Create test instance dirs: one with a valid project-root, one with a stale project-root, one empty projects/ dir
|
|
||||||
4. Call `gc_instances` and verify:
|
|
||||||
- Stale dir was removed
|
|
||||||
- Valid dir was kept
|
|
||||||
- Empty dir produces "0 instance(s) removed"
|
|
||||||
5. Exit 0 on success, exit 1 on failure with diagnostic output
|
|
||||||
|
|
||||||
Make the script executable with `chmod +x test-gc.sh`.
|
|
||||||
</action>
|
|
||||||
<verify>
|
|
||||||
<automated>bash test-gc.sh && echo "GC TESTS PASSED"</automated>
|
|
||||||
</verify>
|
|
||||||
<done>
|
|
||||||
test-gc.sh runs three test cases covering stale removal, valid preservation, and empty-dir safety. All pass.
|
|
||||||
</done>
|
|
||||||
</task>
|
|
||||||
|
|
||||||
</tasks>
|
|
||||||
|
|
||||||
<threat_model>
|
|
||||||
## Trust Boundaries
|
|
||||||
|
|
||||||
| Boundary | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| project-root file content to rm -rf | The path read from project-root is used to decide whether to delete the instance dir; a tampered project-root could prevent GC or cause unexpected behavior |
|
|
||||||
| user CLI input (--gc flag) to filesystem deletion | --gc triggers rm -rf on instance dirs; only dirs under ~/.claudebox/projects/ are affected |
|
|
||||||
|
|
||||||
## STRIDE Threat Register
|
|
||||||
|
|
||||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
|
||||||
|-----------|----------|-----------|-------------|-----------------|
|
|
||||||
| T-05-06 | Tampering | project-root file content | accept | The project-root file is written by claudebox itself and lives under user-owned ~/.claudebox/. A user tampering with their own files is not a threat. GC only deletes the instance dir, not the path recorded in project-root. |
|
|
||||||
| T-05-07 | Denial of Service | --gc deletes wrong dirs | mitigate | GC loop is scoped to `$HOME/.claudebox/projects/*/` only; `rm -rf` target is `$dir` which is always under that prefix. Cannot escape to arbitrary paths. |
|
|
||||||
| T-05-08 | Elevation of Privilege | rm -rf via symlink in projects/ | mitigate | Instance dirs are created by claudebox's own `mkdir -p`; if a symlink appears in projects/, the `[[ -d "$dir" ]]` check follows symlinks but `rm -rf` on a symlink to a directory would delete the symlink target. Low risk since ~/.claudebox/ is user-writable anyway. Accept for personal tool. |
|
|
||||||
</threat_model>
|
|
||||||
|
|
||||||
<verification>
|
|
||||||
1. `bash -n claudebox.sh` passes
|
|
||||||
2. `claudebox --gc` runs without error on current system (prints "GC complete: 0 instance(s) removed." if no stale dirs)
|
|
||||||
3. `bash test-gc.sh` passes all three test cases
|
|
||||||
4. `claudebox --gc` does NOT launch Claude Code (exits immediately)
|
|
||||||
</verification>
|
|
||||||
|
|
||||||
<success_criteria>
|
|
||||||
- `claudebox --gc` removes stale instance directories and exits
|
|
||||||
- Valid instance directories (project root still exists) are preserved
|
|
||||||
- Empty projects/ directory does not cause errors
|
|
||||||
- GC output goes to stderr with removal details and summary count
|
|
||||||
</success_criteria>
|
|
||||||
|
|
||||||
<output>
|
|
||||||
After completion, create `.planning/phases/05-per-project-instance-isolation/05-02-SUMMARY.md`
|
|
||||||
</output>
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
# Phase 5: Per-Project Instance Isolation - Context
|
|
||||||
|
|
||||||
**Gathered:** 2026-04-10 (assumptions mode)
|
|
||||||
**Updated:** 2026-04-13 (PLUGIN_MOUNT_FIX.md integration)
|
|
||||||
**Status:** Ready for planning
|
|
||||||
|
|
||||||
<domain>
|
|
||||||
## Phase Boundary
|
|
||||||
|
|
||||||
Two changes in one phase:
|
|
||||||
|
|
||||||
1. **Plugin mount fix** — Replace `~/.claudebox` symlink approach with direct `~/.claude` mount + overlays. This fixes all plugins, skills, hooks, MCP configs, commands, agents, keybindings, settings being invisible inside the sandbox.
|
|
||||||
|
|
||||||
2. **Per-project isolation** — Each project directory gets its own conversation history and project-scoped memory. Launching from a git worktree shares state with the main worktree of the same repo. Two concurrent sessions in the same project do not corrupt each other. `claudebox --gc` removes instance directories whose project root no longer exists on disk.
|
|
||||||
|
|
||||||
Credentials (OAuth tokens, API keys) are intentionally shared across all instances — not isolated per project.
|
|
||||||
|
|
||||||
</domain>
|
|
||||||
|
|
||||||
<decisions>
|
|
||||||
## Implementation Decisions
|
|
||||||
|
|
||||||
### Mount Architecture Change (from PLUGIN_MOUNT_FIX.md)
|
|
||||||
|
|
||||||
- **D-01:** Replace `--bind ~/.claudebox ~/.claudebox` + `--symlink ~/.claudebox ~/.claude` with `--bind ~/.claude ~/.claude`. This gives the sandbox direct access to all Claude Code config: skills/, commands/, hooks/, plugins/, agents/, mcp.json, settings.json, keybindings.json, get-shit-done/, CLAUDE.md, statusline.sh — no maintenance when new files are added.
|
|
||||||
- **D-02:** Overlay `--bind ~/.claudebox/projects ~/.claude/projects` AFTER the `~/.claude` bind mount (bwrap last-mount-wins). This isolates per-project memory while keeping everything else from real `~/.claude`.
|
|
||||||
- **D-03:** Overlay `--bind ~/.claudebox/history.jsonl ~/.claude/history.jsonl` AFTER the `~/.claude` bind mount. This isolates session history.
|
|
||||||
- **D-04:** Ensure `~/.claudebox/projects/` directory and `~/.claudebox/history.jsonl` file exist at startup (before bwrap).
|
|
||||||
- **D-05:** `.credentials.json` handling stays as-is — already separate in current code.
|
|
||||||
- **D-06:** SANDBOX.md and CLAUDE.md management moves from `~/.claudebox/` to `~/.claude/` (or is dropped if real `~/.claude/CLAUDE.md` already has what's needed). Must not overwrite user's real `~/.claude/CLAUDE.md` destructively.
|
|
||||||
|
|
||||||
### Per-Project Scoping of projects/ Directory
|
|
||||||
|
|
||||||
- **D-07:** `~/.claudebox/projects/` contains per-project subdirs keyed by hash: `~/.claudebox/projects/<16-char-hash>/`. Inside sandbox, these appear at `~/.claude/projects/<hash>/`.
|
|
||||||
- **D-08:** Canonical project root = `git rev-parse --git-common-dir` resolved to absolute path, falling back to `$CWD` for non-git directories. All worktrees of the same repo get the same hash.
|
|
||||||
- **D-09:** Hash = SHA-256 of canonical root path, truncated to 16 hex chars.
|
|
||||||
- **D-10:** On instance creation, write `project-root` plaintext file inside `~/.claudebox/projects/<hash>/` containing the canonical root path.
|
|
||||||
|
|
||||||
### GC Mechanism
|
|
||||||
|
|
||||||
- **D-11:** `claudebox --gc` iterates `~/.claudebox/projects/*/project-root`, reads each path, removes any dir whose recorded path no longer exists on disk. Prints removed paths to stderr.
|
|
||||||
- **D-12:** Add `--gc` to flag-parsing block alongside `--yes`, `--dry-run`, `--check`.
|
|
||||||
|
|
||||||
### Concurrent Session Safety
|
|
||||||
|
|
||||||
- **D-13:** No locking needed. Claude Code manages file-level concurrency within its own data dir. Same instance dir shared by concurrent sessions in same project is fine.
|
|
||||||
|
|
||||||
### CLAUDE_CONFIG_DIR — NOT USED
|
|
||||||
|
|
||||||
- **D-14:** Previous approach using `CLAUDE_CONFIG_DIR` env var is abandoned. Direct mount + overlay is simpler, preserves all plugins/skills/hooks, and requires no per-instance directory tree duplication.
|
|
||||||
|
|
||||||
### Claude's Discretion
|
|
||||||
|
|
||||||
- Exact hash truncation length (16 chars recommended)
|
|
||||||
- Whether to print instance path at launch (verbose addition later)
|
|
||||||
- SANDBOX.md strategy: whether to keep writing it to `~/.claudebox/` and bind-mount it, or write to `~/.claude/` directly
|
|
||||||
- Whether `history.jsonl` needs per-project hashing too or one shared sandbox history is fine
|
|
||||||
|
|
||||||
</decisions>
|
|
||||||
|
|
||||||
<canonical_refs>
|
|
||||||
## Canonical References
|
|
||||||
|
|
||||||
**Downstream agents MUST read these before planning or implementing.**
|
|
||||||
|
|
||||||
### Codebase
|
|
||||||
- `claudebox.sh` — full script; flag parsing (lines 1-18), BWRAP_ARGS construction (lines 364-401), credential mount logic (lines 104-122), SANDBOX.md/CLAUDE.md management (lines 124-175), dry-run echo block (lines 318-361)
|
|
||||||
- `flake.nix` — Nix packaging; runtimeInputs list
|
|
||||||
- `PLUGIN_MOUNT_FIX.md` — the architectural change driving this update
|
|
||||||
|
|
||||||
### Requirements
|
|
||||||
- `.planning/REQUIREMENTS.md` — INST-01 through INST-04 need to be added during planning
|
|
||||||
- `.planning/ROADMAP.md` — Phase 5 success criteria (lines 44-53)
|
|
||||||
|
|
||||||
</canonical_refs>
|
|
||||||
|
|
||||||
<code_context>
|
|
||||||
## Existing Code Insights
|
|
||||||
|
|
||||||
### What Must Change
|
|
||||||
- Lines 383-384: `--bind "$HOME/.claudebox" "$HOME/.claudebox"` + `--symlink "$HOME/.claudebox" "$HOME/.claude"` → `--bind "$HOME/.claude" "$HOME/.claude"` + overlay mounts
|
|
||||||
- Lines 348-349 (dry-run): Same change mirrored in dry-run output
|
|
||||||
- Lines 104-112: Credential file path may need adjusting (`~/.claudebox/.credentials.json` stays but mount target changes)
|
|
||||||
- Lines 124-175: SANDBOX.md/CLAUDE.md generation — rethink since `~/.claude` is now real
|
|
||||||
- Lines 276-283 (print_audit): Update mount display to reflect new architecture
|
|
||||||
- Line 102: `mkdir -p "$HOME/.claudebox"` — also ensure projects/ and history.jsonl exist
|
|
||||||
|
|
||||||
### Reusable Assets
|
|
||||||
- Flag parsing block (lines 1-18): add `--gc` case
|
|
||||||
- `BWRAP_ARGS` array with `+=` appends: add overlay mounts after `~/.claude` bind
|
|
||||||
- Credential detection pattern (lines 104-122): same pattern
|
|
||||||
- `coreutils` already in `runtimeInputs` — provides `sha256sum`
|
|
||||||
|
|
||||||
### Integration Points
|
|
||||||
- Mount order critical: `--bind ~/.claude ~/.claude` first, then overlays for projects/ and history.jsonl
|
|
||||||
- Dry-run block must mirror all changes
|
|
||||||
- print_audit() must reflect new mount layout
|
|
||||||
|
|
||||||
</code_context>
|
|
||||||
|
|
||||||
<specifics>
|
|
||||||
## Specific Ideas
|
|
||||||
|
|
||||||
- Per-project dirs at `~/.claudebox/projects/<16-char-hash>/` — human-debuggable length
|
|
||||||
- GC prints removed paths to stderr (consistent with claudebox's stderr-only output convention)
|
|
||||||
- `--gc` exits after running, does not launch Claude (same pattern as `--check`)
|
|
||||||
- SANDBOX.md could be bind-mounted as single file from `~/.claudebox/SANDBOX.md` → `~/.claude/SANDBOX.md` to avoid touching user's real `~/.claude`
|
|
||||||
|
|
||||||
</specifics>
|
|
||||||
|
|
||||||
<deferred>
|
|
||||||
## Deferred Ideas
|
|
||||||
|
|
||||||
- Per-instance `settings.json` override (project-specific model selection) — Phase 7 (Named Profiles)
|
|
||||||
- `--gc --dry-run` to preview what would be removed — scope creep
|
|
||||||
- `claudebox --list-instances` to show all instances with their project roots — nice-to-have
|
|
||||||
|
|
||||||
</deferred>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Phase: 05-per-project-instance-isolation*
|
|
||||||
*Context gathered: 2026-04-10, updated 2026-04-13*
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
# Phase 5: Per-Project Instance Isolation - Discussion Log (Assumptions Mode)
|
|
||||||
|
|
||||||
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
|
||||||
> Decisions captured in CONTEXT.md — this log preserves the analysis.
|
|
||||||
|
|
||||||
**Date:** 2026-04-10
|
|
||||||
**Phase:** 05-per-project-instance-isolation
|
|
||||||
**Mode:** assumptions
|
|
||||||
**Areas analyzed:** Instance Isolation Mechanism, Project Root Identification, Shared vs Per-Instance Files, GC Mechanism
|
|
||||||
|
|
||||||
## Assumptions Presented
|
|
||||||
|
|
||||||
### Instance Isolation Mechanism
|
|
||||||
| Assumption | Confidence | Evidence |
|
|
||||||
|------------|-----------|----------|
|
|
||||||
| Use `CLAUDE_CONFIG_DIR` env var to redirect Claude state to per-project instance dir | Likely | Research confirmed env var exists in official Claude Code docs; current `~/.claudebox` mount stays intact |
|
|
||||||
|
|
||||||
Alternatives presented:
|
|
||||||
- A (Env var `CLAUDE_CONFIG_DIR`): simpler, no mount changes, caveat `~/.claude.json` not redirectable
|
|
||||||
- B (Mount swap): replace `--bind "$HOME/.claudebox"` with per-instance path, more invasive
|
|
||||||
|
|
||||||
### Project Root Identification
|
|
||||||
| Assumption | Confidence | Evidence |
|
|
||||||
|------------|-----------|----------|
|
|
||||||
| `git rev-parse --git-common-dir` as canonical root for worktree sharing | Likely | `--git-common-dir` returns shared `.git` for all worktrees; `--show-toplevel` would diverge per worktree |
|
|
||||||
|
|
||||||
Alternatives presented:
|
|
||||||
- A (`--git-common-dir`): worktrees share instance — satisfies criterion 2
|
|
||||||
- B (`--show-toplevel`): simpler but violates criterion 2
|
|
||||||
|
|
||||||
### Shared vs Per-Instance Files
|
|
||||||
| Assumption | Confidence | Evidence |
|
|
||||||
|------------|-----------|----------|
|
|
||||||
| Credentials stay shared across all instances, not per-instance | Confident | Phase 4 AUTH-01 locked; OAuth refresh requires single writable path; `~/.claude.json` not redirectable per research |
|
|
||||||
|
|
||||||
### GC Mechanism
|
|
||||||
| Assumption | Confidence | Evidence |
|
|
||||||
|------------|-----------|----------|
|
|
||||||
| `project-root` plaintext file inside instance dir; `--gc` reads and checks if path exists | Likely | SHA-256 not reversible; `sessions/*.json` already uses `"cwd"` field establishing the pattern |
|
|
||||||
|
|
||||||
Alternatives presented:
|
|
||||||
- A (plaintext `project-root` file): simple, debuggable
|
|
||||||
- B (`meta.json` with createdAt): more structure, allows future timestamp-based GC
|
|
||||||
|
|
||||||
## Corrections Made
|
|
||||||
|
|
||||||
No corrections — all assumptions confirmed by user.
|
|
||||||
|
|
||||||
## External Research
|
|
||||||
|
|
||||||
- **`CLAUDE_CONFIG_DIR`:** Confirmed in official Claude Code env-vars documentation. Redirects sessions/settings/todos/plugins. `~/.claude.json` hardcoded to `$HOME`, NOT redirected. Source: Claude Code docs + issues #3833, #1652.
|
|
||||||
- **Auto-resume behavior:** Claude Code does NOT auto-resume on launch. Sessions are fresh by default; `--continue`/`--resume` are explicit opt-in. Source: Claude Code common-workflows docs.
|
|
||||||
|
|
@ -1,403 +0,0 @@
|
||||||
# Phase 5: Per-Project Instance Isolation - Research
|
|
||||||
|
|
||||||
**Researched:** 2026-04-13
|
|
||||||
**Domain:** bash scripting, bubblewrap mounts, Claude Code storage layout
|
|
||||||
**Confidence:** HIGH
|
|
||||||
|
|
||||||
<user_constraints>
|
|
||||||
## User Constraints (from CONTEXT.md)
|
|
||||||
|
|
||||||
### Locked Decisions
|
|
||||||
|
|
||||||
- **D-01:** Replace `--bind ~/.claudebox ~/.claudebox` + `--symlink ~/.claudebox ~/.claude` with `--bind ~/.claude ~/.claude`
|
|
||||||
- **D-02:** Overlay `--bind ~/.claudebox/projects ~/.claude/projects` AFTER the `~/.claude` bind mount (bwrap last-mount-wins). Isolates per-project memory while keeping everything else from real `~/.claude`.
|
|
||||||
- **D-03:** Overlay `--bind ~/.claudebox/history.jsonl ~/.claude/history.jsonl` AFTER the `~/.claude` bind mount. Isolates session prompt history.
|
|
||||||
- **D-04:** Ensure `~/.claudebox/projects/` directory and `~/.claudebox/history.jsonl` file exist at startup (before bwrap).
|
|
||||||
- **D-05:** `.credentials.json` handling stays as-is — already separate in current code.
|
|
||||||
- **D-06:** SANDBOX.md and CLAUDE.md management moves from `~/.claudebox/` to `~/.claude/` approach — must not overwrite user's real `~/.claude/CLAUDE.md` destructively.
|
|
||||||
- **D-07:** `~/.claudebox/projects/` contains per-project subdirs keyed by hash: `~/.claudebox/projects/<16-char-hash>/`. Inside sandbox, these appear at `~/.claude/projects/<hash>/`.
|
|
||||||
- **D-08:** Canonical project root = `git rev-parse --git-common-dir` resolved to absolute path, falling back to `$CWD` for non-git directories. All worktrees of the same repo get the same hash.
|
|
||||||
- **D-09:** Hash = SHA-256 of canonical root path, truncated to 16 hex chars.
|
|
||||||
- **D-10:** On instance creation, write `project-root` plaintext file inside `~/.claudebox/projects/<hash>/` containing the canonical root path.
|
|
||||||
- **D-11:** `claudebox --gc` iterates `~/.claudebox/projects/*/project-root`, reads each path, removes any dir whose recorded path no longer exists on disk. Prints removed paths to stderr.
|
|
||||||
- **D-12:** Add `--gc` to flag-parsing block alongside `--yes`, `--dry-run`, `--check`.
|
|
||||||
- **D-13:** No locking needed. Claude Code manages file-level concurrency within its own data dir.
|
|
||||||
- **D-14:** `CLAUDE_CONFIG_DIR` env var approach is abandoned. Direct mount + overlay is simpler.
|
|
||||||
|
|
||||||
### Claude's Discretion
|
|
||||||
|
|
||||||
- Exact hash truncation length (16 chars recommended)
|
|
||||||
- Whether to print instance path at launch (verbose addition later)
|
|
||||||
- SANDBOX.md strategy: whether to keep writing it to `~/.claudebox/` and bind-mount it, or write to `~/.claude/` directly
|
|
||||||
- Whether `history.jsonl` needs per-project hashing too or one shared sandbox history is fine
|
|
||||||
|
|
||||||
### Deferred Ideas (OUT OF SCOPE)
|
|
||||||
|
|
||||||
- Per-instance `settings.json` override (project-specific model selection) — Phase 7
|
|
||||||
- `--gc --dry-run` to preview what would be removed
|
|
||||||
- `claudebox --list-instances` to show all instances with their project roots
|
|
||||||
</user_constraints>
|
|
||||||
|
|
||||||
<phase_requirements>
|
|
||||||
## Phase Requirements
|
|
||||||
|
|
||||||
INST-01 through INST-04 are NOT yet defined in REQUIREMENTS.md. The CONTEXT.md says they need to be added during planning. The planner should define them based on the Phase 5 success criteria and add them to REQUIREMENTS.md.
|
|
||||||
|
|
||||||
| ID | Description | Research Support |
|
|
||||||
|----|-------------|------------------|
|
|
||||||
| INST-01 | Each project directory has isolated conversation history (no cross-contamination between projects) | D-02 overlay mounts `~/.claudebox/projects/<hash>/` as `~/.claude/projects/`; Claude Code writes per-project subdirs inside |
|
|
||||||
| INST-02 | Git worktrees of the same repo share instance state with the main worktree | D-08: `git rev-parse --git-common-dir` resolved to absolute gives same root for all worktrees |
|
|
||||||
| INST-03 | Two concurrent claudebox sessions in the same project do not corrupt each other's state | D-13: No locking needed; Claude Code handles its own file-level concurrency |
|
|
||||||
| INST-04 | `claudebox --gc` removes instance directories for project roots that no longer exist on disk | D-11/D-12: iterate `project-root` files, remove stale dirs |
|
|
||||||
|
|
||||||
There is also an implicit "plugin fix" requirement (call it PLUG-01 or scope it as part of INST-01): the real `~/.claude/` being mounted means all plugins, skills, hooks, MCP configs, commands, agents, keybindings, and settings.json become visible inside the sandbox. The planner should decide whether to track this separately.
|
|
||||||
</phase_requirements>
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Phase 5 is two coordinated changes: (1) fix the plugin mount architecture so that all Claude Code config files in `~/.claude/` are visible inside the sandbox, and (2) implement per-project isolation so that conversation history and project memory are scoped to the canonical git root.
|
|
||||||
|
|
||||||
The current architecture mounts `~/.claudebox` at `~/.claudebox` and symlinks it to `~/.claude` inside the sandbox. This means any Claude Code config that lives in `~/.claude/` (plugins, skills, hooks, mcp.json, etc.) is invisible — the sandbox only sees `~/.claudebox/` contents. The fix is direct: mount `~/.claude` at `~/.claude`, then overlay only the two paths that need isolation (`projects/` and `history.jsonl`) with content from `~/.claudebox/`.
|
|
||||||
|
|
||||||
Per-project isolation works by mounting `~/.claudebox/projects/<16-char-hash>/` as the entire `~/.claude/projects/` view inside the sandbox. Claude Code writes conversation history and project memory into `~/.claude/projects/`, which under the overlay lands in the project-specific hash dir. Two different projects get different hash dirs, so their histories never mix. Git worktrees of the same repo resolve to the same canonical root (via `git rev-parse --git-common-dir`), so they share the same hash dir.
|
|
||||||
|
|
||||||
**Primary recommendation:** Follow decisions D-01 through D-14 exactly as written. All are verified technically sound by live testing in this environment. The only ambiguity is the SANDBOX.md injection strategy (see Discretion section below).
|
|
||||||
|
|
||||||
## Standard Stack
|
|
||||||
|
|
||||||
### Core
|
|
||||||
|
|
||||||
| Tool | Version | Purpose | Why Standard |
|
|
||||||
|------|---------|---------|--------------|
|
|
||||||
| `bubblewrap` (bwrap) | 0.9.x (nixpkgs) | Sandbox + filesystem isolation | Already in use; verified `--bind` overlay behavior confirmed via live test [VERIFIED: live bwrap test] |
|
|
||||||
| `sha256sum` (coreutils) | GNU coreutils 9.10 | Hash canonical root path | Already in `runtimeInputs`; `printf '%s' "$path" | sha256sum | cut -c1-16` produces stable 16-char hex [VERIFIED: live test] |
|
|
||||||
| `git` | nixpkgs | Resolve canonical root for worktrees | Already in `runtimeInputs`; `git rev-parse --git-common-dir` returns `.git` (relative) for normal repos, absolute path for worktrees [VERIFIED: live test] |
|
|
||||||
| `readlink -f` | coreutils | Resolve relative `.git` to absolute path | Already used in claudebox.sh for NixOS symlink resolution |
|
|
||||||
|
|
||||||
No new packages needed. All required tools are already in `runtimeInputs` in `flake.nix`.
|
|
||||||
|
|
||||||
**Installation:** No changes to `flake.nix` required.
|
|
||||||
|
|
||||||
## Architecture Patterns
|
|
||||||
|
|
||||||
### Recommended Project Structure (on host filesystem)
|
|
||||||
|
|
||||||
```
|
|
||||||
~/.claudebox/
|
|
||||||
├── .credentials.json # OAuth tokens (hard-linked to ~/.claude/.credentials.json)
|
|
||||||
├── SANDBOX.md # Managed by claudebox, injected as overlay
|
|
||||||
├── CLAUDE.md # Managed by claudebox (kept for backward compat, not used)
|
|
||||||
├── history.jsonl # Sandbox-side prompt history (overlays ~/.claude/history.jsonl)
|
|
||||||
└── projects/
|
|
||||||
├── 2458cbe666750168/ # SHA-256[:16] of /home/user/code/myproject
|
|
||||||
│ ├── project-root # plaintext: /home/user/code/myproject
|
|
||||||
│ └── -home-user-code-myproject/ # Claude Code writes here (path-based name)
|
|
||||||
│ ├── *.jsonl # conversation history
|
|
||||||
│ └── memory/ # project memory
|
|
||||||
└── 421ebd2f76562141/ # SHA-256[:16] of /home/user/code/other
|
|
||||||
├── project-root
|
|
||||||
└── -home-user-code-other/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Mount Order (critical)
|
|
||||||
|
|
||||||
```
|
|
||||||
--bind ~/.claude ~/.claude
|
|
||||||
(makes all real ~/.claude/ content visible)
|
|
||||||
--bind ~/.claudebox/projects/<hash>/ ~/.claude/projects/
|
|
||||||
(overlays projects/ with this project's isolated dir)
|
|
||||||
--bind ~/.claudebox/history.jsonl ~/.claude/history.jsonl
|
|
||||||
(overlays history.jsonl with sandbox-side file)
|
|
||||||
--bind ~/.claudebox/SANDBOX.md ~/.claude/SANDBOX.md
|
|
||||||
(injects SANDBOX.md without touching user's CLAUDE.md)
|
|
||||||
--bind ~/.claudebox/.credentials.json ~/.claude/.credentials.json
|
|
||||||
(overlays credentials with claudebox-managed copy)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Last-mount-wins is bwrap's documented behavior.** Verified with live bwrap tests: both subdirectory overlays and file-level overlays work correctly. [VERIFIED: live bwrap tests]
|
|
||||||
|
|
||||||
### Pattern 1: Canonical Root Computation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Source: live testing + D-08 design
|
|
||||||
compute_canonical_root() {
|
|
||||||
local cwd="$1"
|
|
||||||
local git_common
|
|
||||||
git_common=$(git -C "$cwd" rev-parse --git-common-dir 2>/dev/null) || {
|
|
||||||
echo "$cwd"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
# Make absolute if relative (normal repo returns ".git")
|
|
||||||
if [[ "$git_common" != /* ]]; then
|
|
||||||
git_common="$cwd/$git_common"
|
|
||||||
fi
|
|
||||||
dirname "$(readlink -f "$git_common")"
|
|
||||||
}
|
|
||||||
|
|
||||||
CANONICAL_ROOT=$(compute_canonical_root "$CWD")
|
|
||||||
INSTANCE_HASH=$(printf '%s' "$CANONICAL_ROOT" | sha256sum | cut -c1-16)
|
|
||||||
INSTANCE_DIR="$HOME/.claudebox/projects/$INSTANCE_HASH"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why `--git-common-dir` and not `--show-toplevel`:** In a worktree, `--show-toplevel` returns the worktree's own root (not the main repo root). `--git-common-dir` always points to the main repo's `.git` directory, so all worktrees of the same repo get the same canonical root. [VERIFIED: git docs + live testing]
|
|
||||||
|
|
||||||
### Pattern 2: Instance Initialization
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Source: D-04, D-10
|
|
||||||
mkdir -p "$INSTANCE_DIR"
|
|
||||||
HISTORY_FILE="$HOME/.claudebox/history.jsonl"
|
|
||||||
touch "$HISTORY_FILE"
|
|
||||||
|
|
||||||
# Write project-root only on first creation (idempotent)
|
|
||||||
if [[ ! -f "$INSTANCE_DIR/project-root" ]]; then
|
|
||||||
printf '%s\n' "$CANONICAL_ROOT" > "$INSTANCE_DIR/project-root"
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why `touch` for history.jsonl:** bwrap bind mount requires the source path to exist. Attempting to bind-mount a non-existent file produces `bwrap: Can't find source path: No such file or directory` and exits 1. [VERIFIED: live bwrap test]
|
|
||||||
|
|
||||||
### Pattern 3: GC Implementation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Source: D-11, D-12
|
|
||||||
gc_instances() {
|
|
||||||
local removed=0
|
|
||||||
for dir in "$HOME/.claudebox/projects"/*/; do
|
|
||||||
[[ -d "$dir" ]] || continue
|
|
||||||
local root_file="$dir/project-root"
|
|
||||||
[[ -f "$root_file" ]] || continue # skip dirs without tracking file (defensive)
|
|
||||||
local root_path
|
|
||||||
root_path=$(< "$root_file")
|
|
||||||
if [[ ! -d "$root_path" ]]; then
|
|
||||||
rm -rf "$dir"
|
|
||||||
echo "Removed: $dir (project path gone: $root_path)" >&2
|
|
||||||
(( removed++ )) || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "GC complete: $removed instance(s) removed." >&2
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pattern 4: Credential Mount Target Update
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# OLD (current, with symlink architecture):
|
|
||||||
# --bind "$HOME/.claudebox/.credentials.json" "$HOME/.claudebox/.credentials.json"
|
|
||||||
# (visible as ~/.claude/.credentials.json via the symlink)
|
|
||||||
|
|
||||||
# NEW (with direct ~/.claude bind):
|
|
||||||
# --bind "$HOME/.claudebox/.credentials.json" "$HOME/.claude/.credentials.json"
|
|
||||||
# (explicit overlay after the ~/.claude bind mount)
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: on this system `~/.claude/.credentials.json` and `~/.claudebox/.credentials.json` are the same inode (hard links from Phase 4). The overlay still has the correct behavior: it re-mounts the claudebox-managed credential file on top of whatever `~/.claude/.credentials.json` the direct bind exposed. [VERIFIED: inode check]
|
|
||||||
|
|
||||||
### Anti-Patterns to Avoid
|
|
||||||
|
|
||||||
- **Mounting entire `~/.claudebox/projects/` as `~/.claude/projects/`:** This would expose ALL project dirs to every project, defeating isolation. Mount only the per-project hash subdir.
|
|
||||||
- **Using `git rev-parse --show-toplevel` instead of `--git-common-dir`:** `--show-toplevel` returns the worktree root, not the main repo root. Worktrees would get different hashes than the main checkout.
|
|
||||||
- **Skipping `touch ~/.claudebox/history.jsonl`:** bwrap fails with a hard error if the source doesn't exist. The script would crash on first run.
|
|
||||||
- **Writing to `~/.claude/CLAUDE.md`:** With the new mount architecture, `~/.claude` is the user's real config dir. Writing CLAUDE.md there would destructively modify the user's actual config. Use bind-mounted SANDBOX.md instead.
|
|
||||||
|
|
||||||
## Don't Hand-Roll
|
|
||||||
|
|
||||||
| Problem | Don't Build | Use Instead | Why |
|
|
||||||
|---------|-------------|-------------|-----|
|
|
||||||
| Content-addressed storage | Custom hash scheme | `sha256sum \| cut -c1-16` | Already in coreutils; stable, collision-resistant for path strings |
|
|
||||||
| Filesystem overlay/union | Custom copy-on-write | bwrap `--bind` last-mount-wins | Kernel-native, no performance cost, no FUSE dependencies |
|
|
||||||
| File locking for concurrent sessions | Custom lockfile scheme | Nothing (D-13) | Claude Code already manages its own concurrency within its data dir |
|
|
||||||
|
|
||||||
## Common Pitfalls
|
|
||||||
|
|
||||||
### Pitfall 1: bwrap requires source to exist before launch
|
|
||||||
|
|
||||||
**What goes wrong:** Adding `--bind ~/.claudebox/history.jsonl ~/.claude/history.jsonl` without first ensuring the source file exists. bwrap exits 1 with "Can't find source path".
|
|
||||||
**Why it happens:** bwrap performs bind mounts at namespace creation time; the source must be a real, existing path on the host filesystem.
|
|
||||||
**How to avoid:** `touch "$HOME/.claudebox/history.jsonl"` before the bwrap call (already in D-04).
|
|
||||||
**Also applies to:** The per-project hash dir: `mkdir -p "$INSTANCE_DIR"` must happen before bwrap.
|
|
||||||
|
|
||||||
### Pitfall 2: Overlay mount order matters
|
|
||||||
|
|
||||||
**What goes wrong:** Placing the `~/.claude/projects/` overlay bind BEFORE the `~/.claude` bind. The `~/.claude` bind would then overwrite the overlay (last-mount-wins goes the wrong way).
|
|
||||||
**Why it happens:** bwrap processes `--bind` args left to right; the last bind for any given path wins.
|
|
||||||
**How to avoid:** Always: `--bind ~/.claude ~/.claude` FIRST, then overlays.
|
|
||||||
|
|
||||||
### Pitfall 3: Credential mount target path not updated
|
|
||||||
|
|
||||||
**What goes wrong:** Keeping `--bind "$CREDS_FILE" "$HOME/.claudebox/.credentials.json"` after removing the `~/.claudebox` bind and symlink. The target path no longer exists in the sandbox (no `~/.claudebox/` dir is mounted).
|
|
||||||
**How to avoid:** Change target to `"$HOME/.claude/.credentials.json"`.
|
|
||||||
|
|
||||||
### Pitfall 4: CLAUDE.md injection modifies user's real config
|
|
||||||
|
|
||||||
**What goes wrong:** Keeping the current CLAUDE.md injection logic (lines 167-174) that writes to `"$HOME/.claudebox/CLAUDE.md"`. After the mount change, `~/.claudebox/CLAUDE.md` is a host-side file not visible in the sandbox at all. Worse, if the code is updated to write to `~/.claude/CLAUDE.md`, it destructively prepends `@SANDBOX.md` to the user's real config.
|
|
||||||
**How to avoid:** Mount SANDBOX.md as a single-file overlay (`--bind ~/.claudebox/SANDBOX.md ~/.claude/SANDBOX.md`). The user's real `~/.claude/CLAUDE.md` already contains `@SANDBOX.md` on this system.
|
|
||||||
**Warning sign:** If SANDBOX.md content is missing inside the sandbox after the migration.
|
|
||||||
|
|
||||||
### Pitfall 5: Dry-run block not mirrored
|
|
||||||
|
|
||||||
**What goes wrong:** Updating `BWRAP_ARGS` but forgetting the `--dry-run` echo block (lines 319-361). The dry-run output shows the old mount layout.
|
|
||||||
**How to avoid:** The dry-run block must be updated in sync with BWRAP_ARGS. Both blocks need: new `~/.claude` bind, projects overlay, history overlay, SANDBOX.md overlay, updated credentials target.
|
|
||||||
|
|
||||||
### Pitfall 6: Unstaged CLAUDE_JSON changes from Phase 4
|
|
||||||
|
|
||||||
**What goes wrong:** The working tree has uncommitted changes adding `CLAUDE_JSON_FILE` detection and a `--bind $CLAUDE_JSON_FILE $HOME/.claude.json` mount (the `~/.claude.json` file, separate from the `~/.claude/` directory). These changes are not in the last commit.
|
|
||||||
**Context:** `~/.claude.json` is at `$HOME/.claude.json` (not inside `~/.claude/`), so this mount is independent of the Phase 5 architecture changes. The `CLAUDE_JSON_FILE` mount should be incorporated into Phase 5 work since it was left uncommitted from Phase 4.
|
|
||||||
|
|
||||||
### Pitfall 7: Glob on empty projects/ directory
|
|
||||||
|
|
||||||
**What goes wrong:** The GC loop `for dir in "$HOME/.claudebox/projects"/*/; do` — if `projects/` is empty, bash expands the glob literally and the loop runs once with a non-existent path.
|
|
||||||
**How to avoid:** Add `[[ -d "$dir" ]] || continue` as the first statement in the loop (already in the Pattern 3 example above).
|
|
||||||
|
|
||||||
## Code Examples
|
|
||||||
|
|
||||||
### Complete instance initialization block
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Source: D-04, D-08, D-09, D-10
|
|
||||||
|
|
||||||
# Compute canonical project root (worktree-aware)
|
|
||||||
CWD=$(pwd)
|
|
||||||
GIT_COMMON=$(git -C "$CWD" rev-parse --git-common-dir 2>/dev/null) || true
|
|
||||||
if [[ -n "$GIT_COMMON" ]]; then
|
|
||||||
[[ "$GIT_COMMON" != /* ]] && GIT_COMMON="$CWD/$GIT_COMMON"
|
|
||||||
CANONICAL_ROOT=$(dirname "$(readlink -f "$GIT_COMMON")")
|
|
||||||
else
|
|
||||||
CANONICAL_ROOT="$CWD"
|
|
||||||
fi
|
|
||||||
|
|
||||||
INSTANCE_HASH=$(printf '%s' "$CANONICAL_ROOT" | sha256sum | cut -c1-16)
|
|
||||||
INSTANCE_DIR="$HOME/.claudebox/projects/$INSTANCE_HASH"
|
|
||||||
|
|
||||||
# Create instance dir and project-root file
|
|
||||||
mkdir -p "$INSTANCE_DIR"
|
|
||||||
if [[ ! -f "$INSTANCE_DIR/project-root" ]]; then
|
|
||||||
printf '%s\n' "$CANONICAL_ROOT" > "$INSTANCE_DIR/project-root"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure history.jsonl source exists (bwrap requires it)
|
|
||||||
touch "$HOME/.claudebox/history.jsonl"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Relevant BWRAP_ARGS section (after change)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Source: D-01, D-02, D-03, D-05, D-06 (after Phase 5 changes)
|
|
||||||
BWRAP_ARGS=(
|
|
||||||
# ... clearenv, ENV_ARGS, tmpfs, proc, dev, nix, etc ...
|
|
||||||
--tmpfs "$HOME"
|
|
||||||
# Phase 5: mount real ~/.claude (replaces ~/.claudebox bind + symlink)
|
|
||||||
--bind "$HOME/.claude" "$HOME/.claude"
|
|
||||||
# Phase 5: overlay projects/ with this project's isolated dir
|
|
||||||
--bind "$INSTANCE_DIR" "$HOME/.claude/projects"
|
|
||||||
# Phase 5: overlay history.jsonl with sandbox-side file
|
|
||||||
--bind "$HOME/.claudebox/history.jsonl" "$HOME/.claude/history.jsonl"
|
|
||||||
# Phase 5: inject SANDBOX.md as file overlay
|
|
||||||
--bind "$HOME/.claudebox/SANDBOX.md" "$HOME/.claude/SANDBOX.md"
|
|
||||||
)
|
|
||||||
# Credentials overlay (mount target changes from .claudebox to .claude)
|
|
||||||
if [[ "$CREDS_MOUNT" == true ]]; then
|
|
||||||
BWRAP_ARGS+=(--bind "$CREDS_FILE" "$HOME/.claude/.credentials.json")
|
|
||||||
fi
|
|
||||||
# CLAUDE_JSON mount (independent of ~/.claude/ dir)
|
|
||||||
if [[ "$CLAUDE_JSON_MOUNT" == true ]]; then
|
|
||||||
BWRAP_ARGS+=(--bind "$CLAUDE_JSON_FILE" "$HOME/.claude.json")
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
### Flag parsing addition for --gc
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Source: D-12 (add to existing while/case block)
|
|
||||||
GC_MODE=false
|
|
||||||
|
|
||||||
while (( $# > 0 )); do
|
|
||||||
case "$1" in
|
|
||||||
--yes|-y) SKIP_AUDIT=true ;;
|
|
||||||
--dry-run) DRY_RUN=true ;;
|
|
||||||
--check) CHECK_MODE=true ;;
|
|
||||||
--shell) SHELL_MODE=true ;;
|
|
||||||
--gc) GC_MODE=true ;; # NEW
|
|
||||||
--) shift; CLAUDE_ARGS+=("$@"); break ;;
|
|
||||||
*) CLAUDE_ARGS+=("$1") ;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
```
|
|
||||||
|
|
||||||
## Open Questions
|
|
||||||
|
|
||||||
1. **SANDBOX.md injection strategy**
|
|
||||||
- What we know: User's real `~/.claude/CLAUDE.md` already has `@SANDBOX.md` on this system. The CONTEXT.md says D-06 "must not overwrite user's real `~/.claude/CLAUDE.md` destructively".
|
|
||||||
- What's unclear: Should claudebox continue to manage SANDBOX.md content (write it to `~/.claudebox/SANDBOX.md` and bind-mount it), or stop injecting it entirely since the user's CLAUDE.md already has `@SANDBOX.md`?
|
|
||||||
- Recommendation: Continue writing `~/.claudebox/SANDBOX.md` and bind-mounting it as `~/.claude/SANDBOX.md`. This keeps the content claudebox-controlled without touching the user's CLAUDE.md. Remove the CLAUDE.md injection logic (lines 167-174) entirely — the real `~/.claude/CLAUDE.md` already has `@SANDBOX.md`, and we must not touch it.
|
|
||||||
|
|
||||||
2. **Uncommitted CLAUDE_JSON_FILE changes**
|
|
||||||
- What we know: The working tree has uncommitted changes (not in HEAD) adding CLAUDE_JSON mount for `~/.claude.json` (the root-level file, not inside `~/.claude/`). Phase 4 verification passed without this code.
|
|
||||||
- What's unclear: Was this intentionally left uncommitted, or is it Phase 4 work that needs to be committed first?
|
|
||||||
- Recommendation: Incorporate these changes into Phase 5. The `~/.claude.json` mount is independent of Phase 5 architecture changes and is useful (stores auth tokens). The planner should include a task to commit these changes as part of Phase 5 work.
|
|
||||||
|
|
||||||
3. **`~/.claudebox/projects/` overlay vs subdir mount**
|
|
||||||
- This was clarified during research: the design mounts `~/.claudebox/projects/<hash>/` as `~/.claude/projects/` (the per-project subdir AS the entire projects view), not `~/.claudebox/projects/` as `~/.claude/projects/` (which would expose all projects).
|
|
||||||
- Claude Code creates path-based subdirs (e.g., `-home-user-code-myproject/`) INSIDE the mounted projects dir, which lands inside the hash subdir on the host.
|
|
||||||
|
|
||||||
## Environment Availability
|
|
||||||
|
|
||||||
| Dependency | Required By | Available | Version | Fallback |
|
|
||||||
|------------|-------------|-----------|---------|----------|
|
|
||||||
| `sha256sum` (coreutils) | D-09 hash computation | yes | GNU coreutils 9.10 | — |
|
|
||||||
| `git` | D-08 canonical root | yes | nixpkgs git | — |
|
|
||||||
| `readlink -f` (coreutils) | D-08 absolute path | yes | GNU coreutils 9.10 | — |
|
|
||||||
| `bwrap` subdirectory overlay | D-02 projects isolation | yes | 0.9.x (nixpkgs) | — |
|
|
||||||
| `bwrap` file overlay | D-03 history isolation | yes | 0.9.x (nixpkgs) | — |
|
|
||||||
|
|
||||||
All dependencies available. No new packages required in `flake.nix`.
|
|
||||||
|
|
||||||
## State of the Art
|
|
||||||
|
|
||||||
| Old Approach | Current Approach | When Changed | Impact |
|
|
||||||
|--------------|------------------|--------------|--------|
|
|
||||||
| `~/.claudebox` bind + symlink to `~/.claude` | Direct `~/.claude` bind + overlays for isolation paths | Phase 5 (this) | All plugins/skills/hooks become visible in sandbox |
|
|
||||||
| No per-project isolation | Hash-based per-project instance dirs | Phase 5 (this) | Conversation history scoped to git root |
|
|
||||||
| No GC | `--gc` flag removes stale instance dirs | Phase 5 (this) | Prevents unbounded growth of `~/.claudebox/projects/` |
|
|
||||||
|
|
||||||
**Deprecated/outdated:**
|
|
||||||
- `--symlink ~/.claudebox ~/.claude` in BWRAP_ARGS: replaced by direct `--bind ~/.claude ~/.claude`
|
|
||||||
- CLAUDE.md injection logic (lines 167-174): no longer needed; user's real CLAUDE.md already has `@SANDBOX.md`
|
|
||||||
- Target path `~/.claudebox/.credentials.json` for credentials overlay: changes to `~/.claude/.credentials.json`
|
|
||||||
|
|
||||||
## Assumptions Log
|
|
||||||
|
|
||||||
| # | Claim | Section | Risk if Wrong |
|
|
||||||
|---|-------|---------|---------------|
|
|
||||||
| A1 | Claude Code manages its own file-level concurrency within its data dir (D-13) | Architecture Patterns | If wrong, concurrent sessions in same project could corrupt data; but since D-13 is a locked decision, implementation proceeds without locking |
|
|
||||||
| A2 | User's `~/.claude/CLAUDE.md` already contains `@SANDBOX.md` on all deployments | Open Questions | SANDBOX.md content wouldn't be injected if user doesn't have `@SANDBOX.md` in their CLAUDE.md; mitigated by the SANDBOX.md bind-mount overlay |
|
|
||||||
|
|
||||||
**All other claims in this research were verified via live testing or direct inspection of the codebase.**
|
|
||||||
|
|
||||||
## Sources
|
|
||||||
|
|
||||||
### Primary (HIGH confidence)
|
|
||||||
- Live bwrap testing — `--bind` overlay behavior, subdirectory overlay under bound parent, file overlay, behavior when source doesn't exist
|
|
||||||
- `claude --help` output — confirmed no `--data-dir` flag exists; Claude Code v2.1.97
|
|
||||||
- `~/.claude/projects/` directory inspection — confirmed path-based naming convention (e.g., `-home-toph-code-tools-claudebox/`)
|
|
||||||
- `~/.claude/history.jsonl` content inspection — confirmed it stores prompt display history with project path references
|
|
||||||
- Inode inspection — confirmed `~/.claude/.credentials.json` and `~/.claudebox/.credentials.json` are the same inode (hard links)
|
|
||||||
- `sha256sum` availability — confirmed in coreutils 9.10 already in `runtimeInputs`
|
|
||||||
- `git rev-parse --git-common-dir` behavior — confirmed `.git` (relative) for normal repos, resolves correctly via `readlink -f`
|
|
||||||
- claudebox.sh source (working tree) — read completely; all line numbers referenced in CONTEXT.md verified
|
|
||||||
- flake.nix — confirmed `runtimeInputs`; no new packages needed
|
|
||||||
|
|
||||||
### Secondary (MEDIUM confidence)
|
|
||||||
- PLUGIN_MOUNT_FIX.md — architectural rationale document confirming the plugin visibility problem and overlay fix
|
|
||||||
- CONTEXT.md decisions D-01 through D-14 — user-validated design decisions from discuss-phase
|
|
||||||
|
|
||||||
## Metadata
|
|
||||||
|
|
||||||
**Confidence breakdown:**
|
|
||||||
- Standard stack: HIGH — all tools verified live on this system
|
|
||||||
- Architecture: HIGH — bwrap overlay behavior verified with live tests; Claude Code storage layout confirmed by inspection
|
|
||||||
- Pitfalls: HIGH — most pitfalls discovered via direct testing or code inspection, not assumption
|
|
||||||
|
|
||||||
**Research date:** 2026-04-13
|
|
||||||
**Valid until:** 2026-07-13 (stable APIs; Claude Code storage format could change on any release)
|
|
||||||
106
claudebox.sh
106
claudebox.sh
|
|
@ -3,6 +3,7 @@ SKIP_AUDIT=false
|
||||||
DRY_RUN=false
|
DRY_RUN=false
|
||||||
CHECK_MODE=false
|
CHECK_MODE=false
|
||||||
SHELL_MODE=false
|
SHELL_MODE=false
|
||||||
|
GC_MODE=false
|
||||||
CLAUDE_ARGS=()
|
CLAUDE_ARGS=()
|
||||||
|
|
||||||
while (( $# > 0 )); do
|
while (( $# > 0 )); do
|
||||||
|
|
@ -11,6 +12,7 @@ while (( $# > 0 )); do
|
||||||
--dry-run) DRY_RUN=true ;;
|
--dry-run) DRY_RUN=true ;;
|
||||||
--check) CHECK_MODE=true ;;
|
--check) CHECK_MODE=true ;;
|
||||||
--shell) SHELL_MODE=true ;;
|
--shell) SHELL_MODE=true ;;
|
||||||
|
--gc) GC_MODE=true ;;
|
||||||
--) shift; CLAUDE_ARGS+=("$@"); break ;;
|
--) shift; CLAUDE_ARGS+=("$@"); break ;;
|
||||||
*) CLAUDE_ARGS+=("$1") ;;
|
*) CLAUDE_ARGS+=("$1") ;;
|
||||||
esac
|
esac
|
||||||
|
|
@ -18,6 +20,29 @@ while (( $# > 0 )); do
|
||||||
done
|
done
|
||||||
export SKIP_AUDIT # consumed by Plan 02 audit display
|
export SKIP_AUDIT # consumed by Plan 02 audit display
|
||||||
|
|
||||||
|
# Garbage-collect stale instance directories (D-11, INST-04)
|
||||||
|
gc_instances() {
|
||||||
|
local removed=0
|
||||||
|
local projects_dir="$HOME/.claudebox/projects"
|
||||||
|
if [[ ! -d "$projects_dir" ]]; then
|
||||||
|
echo "No projects directory found at $projects_dir" >&2
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
for dir in "$projects_dir"/*/; do
|
||||||
|
[[ -d "$dir" ]] || continue
|
||||||
|
local root_file="$dir/project-root"
|
||||||
|
[[ -f "$root_file" ]] || continue
|
||||||
|
local root_path
|
||||||
|
root_path=$(< "$root_file")
|
||||||
|
if [[ ! -d "$root_path" ]]; then
|
||||||
|
rm -rf "$dir"
|
||||||
|
echo "Removed: $dir (project root gone: $root_path)" >&2
|
||||||
|
(( removed++ )) || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "GC complete: $removed instance(s) removed." >&2
|
||||||
|
}
|
||||||
|
|
||||||
# --check: verify prerequisites and exit (D-10, UX-05)
|
# --check: verify prerequisites and exit (D-10, UX-05)
|
||||||
if [[ "$CHECK_MODE" == true ]]; then
|
if [[ "$CHECK_MODE" == true ]]; then
|
||||||
pass=true
|
pass=true
|
||||||
|
|
@ -62,6 +87,12 @@ if [[ "$CHECK_MODE" == true ]]; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --gc: remove stale instance directories and exit (D-12, INST-04)
|
||||||
|
if [[ "$GC_MODE" == true ]]; then
|
||||||
|
gc_instances
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# ANSI formatting (D-03)
|
# ANSI formatting (D-03)
|
||||||
if [[ -t 2 ]] && [[ "${NO_COLOR:-}" == "" ]]; then
|
if [[ -t 2 ]] && [[ "${NO_COLOR:-}" == "" ]]; then
|
||||||
BOLD=$'\033[1m'
|
BOLD=$'\033[1m'
|
||||||
|
|
@ -98,12 +129,39 @@ CLAUDE_BIN="$(command -v claude)"
|
||||||
# Record CWD
|
# Record CWD
|
||||||
CWD=$(pwd)
|
CWD=$(pwd)
|
||||||
|
|
||||||
|
# Compute canonical project root — worktree-aware (D-08, INST-02)
|
||||||
|
compute_canonical_root() {
|
||||||
|
local cwd="$1"
|
||||||
|
local git_common
|
||||||
|
git_common=$(git -C "$cwd" rev-parse --git-common-dir 2>/dev/null) || {
|
||||||
|
echo "$cwd"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
# git returns relative ".git" for normal repos; make absolute
|
||||||
|
if [[ "$git_common" != /* ]]; then
|
||||||
|
git_common="$cwd/$git_common"
|
||||||
|
fi
|
||||||
|
dirname "$(readlink -f "$git_common")"
|
||||||
|
}
|
||||||
|
|
||||||
# Ensure ~/.claudebox exists
|
# Ensure ~/.claudebox exists
|
||||||
mkdir -p "$HOME/.claudebox"
|
mkdir -p "$HOME/.claudebox"
|
||||||
|
|
||||||
|
# Per-project instance isolation (D-04, D-07, D-09, D-10, INST-01)
|
||||||
|
CANONICAL_ROOT=$(compute_canonical_root "$CWD")
|
||||||
|
INSTANCE_HASH=$(printf '%s' "$CANONICAL_ROOT" | sha256sum | cut -c1-16)
|
||||||
|
INSTANCE_DIR="$HOME/.claudebox/projects/$INSTANCE_HASH"
|
||||||
|
|
||||||
|
mkdir -p "$INSTANCE_DIR"
|
||||||
|
if [[ ! -f "$INSTANCE_DIR/project-root" ]]; then
|
||||||
|
printf '%s\n' "$CANONICAL_ROOT" > "$INSTANCE_DIR/project-root"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure history.jsonl source exists — bwrap bind requires source to exist (D-04)
|
||||||
|
touch "$HOME/.claudebox/history.jsonl"
|
||||||
|
|
||||||
# Credential file mount (AUTH-01, AUTH-02)
|
# Credential file mount (AUTH-01, AUTH-02)
|
||||||
# Use ~/.claudebox (the host-side claudebox config dir), not ~/.claude
|
# Credential file lives in ~/.claudebox on the host; mounted into sandbox at ~/.claude/.credentials.json
|
||||||
# ~/.claude -> ~/.claudebox symlink only exists inside the sandbox at runtime
|
|
||||||
CREDS_FILE="$HOME/.claudebox/.credentials.json"
|
CREDS_FILE="$HOME/.claudebox/.credentials.json"
|
||||||
if [[ -f "$CREDS_FILE" ]]; then
|
if [[ -f "$CREDS_FILE" ]]; then
|
||||||
CREDS_MOUNT=true
|
CREDS_MOUNT=true
|
||||||
|
|
@ -129,8 +187,8 @@ cat > "$HOME/.claudebox/SANDBOX.md" << 'SANDBOXEOF'
|
||||||
|
|
||||||
You are running inside a bubblewrap (bwrap) sandbox managed by claudebox.
|
You are running inside a bubblewrap (bwrap) sandbox managed by claudebox.
|
||||||
Your filesystem is isolated -- only the current working directory and
|
Your filesystem is isolated -- only the current working directory and
|
||||||
essential system paths are mounted. Both ~/.claude and ~/.claudebox
|
essential system paths are mounted. Your ~/.claude directory is bind-mounted
|
||||||
point to the same directory inside the sandbox.
|
from the host, with per-project isolation for conversation history.
|
||||||
|
|
||||||
## Installing Tools
|
## Installing Tools
|
||||||
|
|
||||||
|
|
@ -163,16 +221,6 @@ For remote operations, prefer HTTPS URLs over SSH since SSH keys
|
||||||
are not available by default.
|
are not available by default.
|
||||||
SANDBOXEOF
|
SANDBOXEOF
|
||||||
|
|
||||||
# Ensure CLAUDE.md has @SANDBOX.md import (D-03, D-08, AWARE-01)
|
|
||||||
CLAUDEMD="$HOME/.claudebox/CLAUDE.md"
|
|
||||||
if [[ ! -f "$CLAUDEMD" ]]; then
|
|
||||||
printf '%s\n' "@SANDBOX.md" > "$CLAUDEMD"
|
|
||||||
elif [[ "$(head -1 "$CLAUDEMD")" != "@SANDBOX.md" ]]; then
|
|
||||||
tmp=$(mktemp)
|
|
||||||
{ printf '%s\n' "@SANDBOX.md"; cat "$CLAUDEMD"; } > "$tmp"
|
|
||||||
mv "$tmp" "$CLAUDEMD"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Generate minimal .gitconfig (D-05)
|
# Generate minimal .gitconfig (D-05)
|
||||||
GIT_NAME=$(git config --global user.name 2>/dev/null || echo "Claude User")
|
GIT_NAME=$(git config --global user.name 2>/dev/null || echo "Claude User")
|
||||||
GIT_EMAIL=$(git config --global user.email 2>/dev/null || echo "claude@localhost")
|
GIT_EMAIL=$(git config --global user.email 2>/dev/null || echo "claude@localhost")
|
||||||
|
|
@ -274,10 +322,10 @@ print_audit() {
|
||||||
# Mounts section
|
# Mounts section
|
||||||
echo "${BOLD}Mounts:${RESET}" >&2
|
echo "${BOLD}Mounts:${RESET}" >&2
|
||||||
printf ' %-12s %s (read-write)\n' "CWD" "$CWD" >&2
|
printf ' %-12s %s (read-write)\n' "CWD" "$CWD" >&2
|
||||||
printf ' %-12s %s (read-write)\n' "$HOME/.claude" "$HOME/.claudebox" >&2
|
printf ' %-12s %s (read-write)\n' "$HOME/.claude" "$HOME/.claude" >&2
|
||||||
if [[ "$CLAUDE_JSON_MOUNT" == true ]]; then
|
printf ' %-12s %s (read-write, project: %s)\n' "projects/" "$INSTANCE_DIR" "$CANONICAL_ROOT" >&2
|
||||||
printf ' %-12s %s (read-write)\n' "$HOME/.claude.json" "$CLAUDE_JSON_FILE" >&2
|
printf ' %-12s %s (read-write)\n' "history" "$HOME/.claudebox/history.jsonl" >&2
|
||||||
fi
|
printf ' %-12s %s (read-only overlay)\n' "SANDBOX.md" "$HOME/.claudebox/SANDBOX.md" >&2
|
||||||
if [[ "$CREDS_MOUNT" == true ]]; then
|
if [[ "$CREDS_MOUNT" == true ]]; then
|
||||||
printf ' %-12s %s (read-write)\n' "credentials" "$CREDS_FILE" >&2
|
printf ' %-12s %s (read-write)\n' "credentials" "$CREDS_FILE" >&2
|
||||||
fi
|
fi
|
||||||
|
|
@ -344,14 +392,17 @@ if [[ "$DRY_RUN" == true ]]; then
|
||||||
echo " --ro-bind /etc/nsswitch.conf /etc/nsswitch.conf \\"
|
echo " --ro-bind /etc/nsswitch.conf /etc/nsswitch.conf \\"
|
||||||
echo " --ro-bind /etc/nix /etc/nix \\"
|
echo " --ro-bind /etc/nix /etc/nix \\"
|
||||||
printf ' --symlink %q /usr/bin/env \\\n' "$(readlink -f "$(command -v env)")"
|
printf ' --symlink %q /usr/bin/env \\\n' "$(readlink -f "$(command -v env)")"
|
||||||
|
printf ' --symlink %q /bin/sh \\\n' "$(readlink -f "$(command -v bash)")"
|
||||||
echo " --tmpfs $HOME \\"
|
echo " --tmpfs $HOME \\"
|
||||||
echo " --bind $HOME/.claudebox $HOME/.claudebox \\"
|
echo " --bind $HOME/.claude $HOME/.claude \\"
|
||||||
echo " --symlink $HOME/.claudebox $HOME/.claude \\"
|
echo " --bind $INSTANCE_DIR $HOME/.claude/projects \\"
|
||||||
|
echo " --bind $HOME/.claudebox/history.jsonl $HOME/.claude/history.jsonl \\"
|
||||||
|
echo " --bind $HOME/.claudebox/SANDBOX.md $HOME/.claude/SANDBOX.md \\"
|
||||||
if [[ "$CLAUDE_JSON_MOUNT" == true ]]; then
|
if [[ "$CLAUDE_JSON_MOUNT" == true ]]; then
|
||||||
echo " --bind $CLAUDE_JSON_FILE $HOME/.claude.json \\"
|
echo " --bind $CLAUDE_JSON_FILE $HOME/.claude.json \\"
|
||||||
fi
|
fi
|
||||||
if [[ "$CREDS_MOUNT" == true ]]; then
|
if [[ "$CREDS_MOUNT" == true ]]; then
|
||||||
echo " --bind $CREDS_FILE $HOME/.claudebox/.credentials.json \\"
|
echo " --bind $CREDS_FILE $HOME/.claude/.credentials.json \\"
|
||||||
fi
|
fi
|
||||||
printf ' --ro-bind %q %s/.gitconfig \\\n' "$GITCONFIG_TMP" "$HOME"
|
printf ' --ro-bind %q %s/.gitconfig \\\n' "$GITCONFIG_TMP" "$HOME"
|
||||||
echo " --bind $CWD $CWD \\"
|
echo " --bind $CWD $CWD \\"
|
||||||
|
|
@ -379,15 +430,22 @@ BWRAP_ARGS=(
|
||||||
--ro-bind /etc/nsswitch.conf /etc/nsswitch.conf
|
--ro-bind /etc/nsswitch.conf /etc/nsswitch.conf
|
||||||
--ro-bind /etc/nix /etc/nix
|
--ro-bind /etc/nix /etc/nix
|
||||||
--symlink "$(readlink -f "$(command -v env)")" /usr/bin/env
|
--symlink "$(readlink -f "$(command -v env)")" /usr/bin/env
|
||||||
|
--symlink "$(readlink -f "$(command -v bash)")" /bin/sh
|
||||||
--tmpfs "$HOME"
|
--tmpfs "$HOME"
|
||||||
--bind "$HOME/.claudebox" "$HOME/.claudebox"
|
# Phase 5: direct ~/.claude bind (D-01) — all plugins/skills/hooks/MCP visible
|
||||||
--symlink "$HOME/.claudebox" "$HOME/.claude"
|
--bind "$HOME/.claude" "$HOME/.claude"
|
||||||
|
# Phase 5: overlay projects/ with per-project isolated dir (D-02, INST-01)
|
||||||
|
--bind "$INSTANCE_DIR" "$HOME/.claude/projects"
|
||||||
|
# Phase 5: overlay history.jsonl with sandbox-side file (D-03)
|
||||||
|
--bind "$HOME/.claudebox/history.jsonl" "$HOME/.claude/history.jsonl"
|
||||||
|
# Phase 5: inject SANDBOX.md as file overlay (D-06)
|
||||||
|
--bind "$HOME/.claudebox/SANDBOX.md" "$HOME/.claude/SANDBOX.md"
|
||||||
)
|
)
|
||||||
if [[ "$CLAUDE_JSON_MOUNT" == true ]]; then
|
if [[ "$CLAUDE_JSON_MOUNT" == true ]]; then
|
||||||
BWRAP_ARGS+=(--bind "$CLAUDE_JSON_FILE" "$HOME/.claude.json")
|
BWRAP_ARGS+=(--bind "$CLAUDE_JSON_FILE" "$HOME/.claude.json")
|
||||||
fi
|
fi
|
||||||
if [[ "$CREDS_MOUNT" == true ]]; then
|
if [[ "$CREDS_MOUNT" == true ]]; then
|
||||||
BWRAP_ARGS+=(--bind "$CREDS_FILE" "$HOME/.claudebox/.credentials.json")
|
BWRAP_ARGS+=(--bind "$CREDS_FILE" "$HOME/.claude/.credentials.json")
|
||||||
fi
|
fi
|
||||||
BWRAP_ARGS+=(
|
BWRAP_ARGS+=(
|
||||||
--ro-bind "$GITCONFIG_TMP" "$HOME/.gitconfig"
|
--ro-bind "$GITCONFIG_TMP" "$HOME/.gitconfig"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue