claudebox/.planning/phases/05-per-project-instance-isolation/05-02-PLAN.md

10 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
05-per-project-instance-isolation 02 execute 2
05-01
claudebox.sh
test-gc.sh
true
INST-04
truths artifacts key_links
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
path provides contains
claudebox.sh gc_instances function and --gc flag handling gc_instances
path provides contains
claudebox.sh --gc in flag parsing case statement --gc)
path provides contains
test-gc.sh GC integration test covering stale removal, valid preservation, empty-dir safety gc_instances
from to via pattern
claudebox.sh (--gc flag) gc_instances function GC_MODE=true triggers gc_instances call GC_MODE.*gc_instances
from to via pattern
gc_instances ~/.claudebox/projects/*/project-root reads project-root file, checks if path exists on disk project-root
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.

<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>

@.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 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):

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
Task 1: Add --gc flag and gc_instances function claudebox.sh - 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) Modify claudebox.sh with these changes:

1. Add GC_MODE flag variable (line 5, after SHELL_MODE=false):

GC_MODE=false

2. Add --gc case to flag parsing (inside the while/case block, after the --shell) case):

    --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:

# 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:

# --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). 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" <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> 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.

Task 2: Add GC integration test test-gc.sh - claudebox.sh (the gc_instances function from Task 1) - 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" 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. bash test-gc.sh && echo "GC TESTS PASSED" test-gc.sh runs three test cases covering stale removal, valid preservation, and empty-dir safety. All pass.

<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>
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)

<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>
After completion, create `.planning/phases/05-per-project-instance-isolation/05-02-SUMMARY.md`