docs(05): create phase plan — mount rewrite + per-project isolation + GC
This commit is contained in:
parent
a040aaa58a
commit
dd064aa858
3 changed files with 571 additions and 2 deletions
|
|
@ -50,7 +50,10 @@ Plans:
|
|||
2. Launching claudebox from a git worktree shares instance state with the main worktree of the same repo
|
||||
3. Two concurrent claudebox sessions in the same project do not corrupt each other's state
|
||||
4. Running `claudebox --gc` removes instance directories for project roots that no longer exist on disk
|
||||
**Plans**: TBD
|
||||
**Plans**: 2 plans
|
||||
Plans:
|
||||
- [ ] 05-01-PLAN.md — Mount architecture rewrite + per-project isolation
|
||||
- [ ] 05-02-PLAN.md — GC mechanism + integration test
|
||||
|
||||
### Phase 6: Tiered Network Isolation
|
||||
**Goal**: Users can select a network access tier at launch to control whether Claude has no network, internet-only, or full host network access
|
||||
|
|
@ -84,6 +87,6 @@ Plans:
|
|||
| 2. Env Audit and CLI Polish | v1.0 | 2/2 | Complete | 2026-04-09 |
|
||||
| 3. Sandbox-Aware Prompting | v1.0 | 1/1 | Complete | 2026-04-10 |
|
||||
| 4. Auth Passthrough | v2.0 | 1/1 | Complete | 2026-04-10 |
|
||||
| 5. Per-Project Instance Isolation | v2.0 | 0/? | Not started | - |
|
||||
| 5. Per-Project Instance Isolation | v2.0 | 0/2 | In progress | - |
|
||||
| 6. Tiered Network Isolation | v2.0 | 0/? | Not started | - |
|
||||
| 7. Named Profiles | v2.0 | 0/? | Not started | - |
|
||||
|
|
|
|||
324
.planning/phases/05-per-project-instance-isolation/05-01-PLAN.md
Normal file
324
.planning/phases/05-per-project-instance-isolation/05-01-PLAN.md
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
---
|
||||
phase: 05-per-project-instance-isolation
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- claudebox.sh
|
||||
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"
|
||||
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\""
|
||||
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, and implement per-project instance isolation.
|
||||
|
||||
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.
|
||||
</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
|
||||
|
||||
<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 (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 (credential mount, lines 104-122):
|
||||
```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, 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)
|
||||
- .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. 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.
|
||||
|
||||
**6. 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 [[ "$CREDS_MOUNT" == true ]]; then
|
||||
echo " --bind $CREDS_FILE $HOME/.claude/.credentials.json \\"
|
||||
fi
|
||||
```
|
||||
|
||||
**7. 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
|
||||
```
|
||||
|
||||
**8. 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 -- '--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 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`
|
||||
- print_audit shows `projects/` mount line with `$INSTANCE_DIR` and `$CANONICAL_ROOT`
|
||||
- SANDBOX.md heredoc does NOT contain `~/.claudebox`
|
||||
</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, and all display/dry-run blocks updated to match. Old symlink approach completely removed.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 2: 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.</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 --yes` briefly (Ctrl+C after launch) — verify Claude Code starts and plugins/skills/hooks are visible (check with `ls ~/.claude/` inside sandbox if using --shell mode)
|
||||
</how-to-verify>
|
||||
<verify>Human confirms all 5 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 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. Two different project dirs produce different INSTANCE_HASH values
|
||||
4. `~/.claudebox/projects/<hash>/project-root` contains the correct canonical root
|
||||
5. 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
|
||||
- Dry-run and audit display reflect new mount layout
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/05-per-project-instance-isolation/05-01-SUMMARY.md`
|
||||
</output>
|
||||
242
.planning/phases/05-per-project-instance-isolation/05-02-PLAN.md
Normal file
242
.planning/phases/05-per-project-instance-isolation/05-02-PLAN.md
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
---
|
||||
phase: 05-per-project-instance-isolation
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- 05-01
|
||||
files_modified:
|
||||
- claudebox.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)"
|
||||
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.
|
||||
</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>
|
||||
Loading…
Add table
Reference in a new issue