diff --git a/.planning/phases/05-per-project-instance-isolation/05-RESEARCH.md b/.planning/phases/05-per-project-instance-isolation/05-RESEARCH.md new file mode 100644 index 0000000..d061ae6 --- /dev/null +++ b/.planning/phases/05-per-project-instance-isolation/05-RESEARCH.md @@ -0,0 +1,403 @@ +# Phase 5: Per-Project Instance Isolation - Research + +**Researched:** 2026-04-13 +**Domain:** bash scripting, bubblewrap mounts, Claude Code storage layout +**Confidence:** HIGH + + +## 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//`. +- **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//` 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 + + + +## 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//` 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. + + +## 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// ~/.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//` 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)