diff --git a/.planning/phases/03-sandbox-aware-prompting/03-RESEARCH.md b/.planning/phases/03-sandbox-aware-prompting/03-RESEARCH.md new file mode 100644 index 0000000..0b5b53c --- /dev/null +++ b/.planning/phases/03-sandbox-aware-prompting/03-RESEARCH.md @@ -0,0 +1,280 @@ +# Phase 3: Sandbox-Aware Prompting - Research + +**Researched:** 2026-04-09 +**Domain:** Shell scripting (heredoc generation, file manipulation), Claude Code memory system +**Confidence:** HIGH + +## Summary + +Phase 3 adds two files to `~/.claudebox/` on every claudebox launch: a fully-managed `SANDBOX.md` containing sandbox context, and a user-owned `CLAUDE.md` that imports it via Claude Code's `@` syntax. Since `~/.claudebox` is bind-mounted as `~/.claude` inside the sandbox (SAND-08, already implemented), Claude Code automatically loads both files at session start. + +The implementation is straightforward shell scripting -- a heredoc write for SANDBOX.md and a head/grep check for the CLAUDE.md import line. The main research concern was verifying that `@SANDBOX.md` relative imports work correctly from `~/.claude/CLAUDE.md`. Official documentation confirms relative paths resolve relative to the containing file, which is the behavior we need. There are known issues with tilde-expansion imports (`@~/.claude/foo.md`) but our case uses a simple filename in the same directory, which is the simplest and most reliable form. + +**Primary recommendation:** Implement as a single contiguous block in `claudebox.sh` between the existing `mkdir -p "$HOME/.claudebox"` (line 102) and the gitconfig generation (line 104). Use a heredoc for SANDBOX.md content and simple `head -1` / `grep` for the CLAUDE.md import check. + + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions +- **D-01:** Two-file approach. claudebox manages `~/.claudebox/SANDBOX.md` (sandbox context) and ensures `~/.claudebox/CLAUDE.md` has an `@SANDBOX.md` import at the top. Since `~/.claudebox` is bind-mounted as `~/.claude` inside the sandbox, Claude Code auto-loads both files at session start. +- **D-02:** `SANDBOX.md` is fully owned by claudebox -- overwritten on every launch. User should not edit this file; changes are lost on next run. +- **D-03:** `CLAUDE.md` is user-owned. claudebox only ensures the `@SANDBOX.md` import line exists at the top. If missing, it's re-added. All other content is untouched. +- **D-04:** Friendly guide tone -- short prose paragraphs, not terse bullets. Sections: sandbox overview, installing tools (comma + nix shell with examples), default restrictions (phrased as "by default, not mounted" to avoid contradicting user customizations), git setup. +- **D-05:** Default restrictions use "by default" phrasing: "By default, the following are not mounted into the sandbox: SSH keys, GPG/age keys, cloud credentials, Tailscale." Includes note: "If your setup has been customized, some of these may be available." +- **D-06:** Git section notes identity is pre-configured (name/email) and suggests HTTPS for remotes by default. Mentions safe.directory is set. +- **D-07:** Uses Claude Code's `@path` import syntax in CLAUDE.md. `@SANDBOX.md` at the first line. This is auto-expanded at session start -- no Read tool call needed. +- **D-08:** On every launch, claudebox: (1) writes/overwrites `~/.claudebox/SANDBOX.md` with current content, (2) checks if `~/.claudebox/CLAUDE.md` exists -- creates it with just the import line if not, (3) if CLAUDE.md exists, checks first line for the `@SANDBOX.md` import -- prepends it if missing. + +### Claude's Discretion +- Exact prose wording and section ordering in SANDBOX.md +- How the first-line check works in shell (grep, head, etc.) +- Whether to use a comment marker around the import line for robustness + +### Deferred Ideas (OUT OF SCOPE) +None -- discussion stayed within phase scope + + + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| AWARE-01 | Default `CLAUDE.md` is created in `~/.claudebox/` on first run if not present | D-08 defines exact behavior; shell pattern for conditional file creation is standard | +| AWARE-02 | Injected CLAUDE.md tells Claude it's in a sandbox, how to use comma/nix for tools, and what's not available | D-04/D-05/D-06 define content structure; `@SANDBOX.md` import mechanism verified against official docs | + + + +## Standard Stack + +No new libraries or packages. This phase is pure shell scripting within the existing `claudebox.sh` and `flake.nix` structure. + +| Tool | Purpose | Already Available | +|------|---------|-------------------| +| `cat` (heredoc) | Write SANDBOX.md content | Yes (coreutils in runtimeInputs) | +| `head` | Check first line of CLAUDE.md | Yes (coreutils) | +| `grep` | Pattern match for import line | Available but not in runtimeInputs -- use shell builtins instead | +| `sed` | Prepend line to file | Available but not in runtimeInputs -- use temp file + cat instead | + +**Key constraint:** The file manipulation runs on the HOST before `exec bwrap`, so host tools are available. No need to worry about sandbox PATH limitations for this code. + +## Architecture Patterns + +### Integration Point in claudebox.sh + +The new code inserts between line 102 (`mkdir -p "$HOME/.claudebox"`) and line 104 (gitconfig generation). This is the natural location because: +1. `~/.claudebox` directory is guaranteed to exist +2. File generation happens before the bwrap exec +3. Groups all pre-launch setup together + +``` +# Existing line 102 +mkdir -p "$HOME/.claudebox" + +# NEW: SANDBOX.md generation (D-02) +# NEW: CLAUDE.md import check (D-03, D-08) + +# Existing line 104+ +GIT_NAME=$(git config --global user.name ...) +``` + +### Pattern: Heredoc for SANDBOX.md + +Write SANDBOX.md using a heredoc. This keeps content inline in claudebox.sh (as noted in CONTEXT.md specifics section) with no extra files in the derivation. + +```bash +# Write SANDBOX.md (overwritten every launch per D-02) +cat > "$HOME/.claudebox/SANDBOX.md" << 'SANDBOXEOF' +# Sandbox Environment + +You are running inside a bubblewrap (bwrap) sandbox... +SANDBOXEOF +``` + +Use single-quoted heredoc delimiter (`'SANDBOXEOF'`) to prevent variable expansion -- the SANDBOX.md content is static text, no shell variables needed. [VERIFIED: standard bash heredoc behavior] + +### Pattern: CLAUDE.md Import Check + +```bash +# Ensure CLAUDE.md has @SANDBOX.md import (D-03, D-08) +CLAUDEMD="$HOME/.claudebox/CLAUDE.md" +IMPORT_LINE="@SANDBOX.md" + +if [[ ! -f "$CLAUDEMD" ]]; then + # First run: create with just the import line + echo "$IMPORT_LINE" > "$CLAUDEMD" +elif ! head -1 "$CLAUDEMD" | grep -qF "$IMPORT_LINE"; then + # Exists but missing import: prepend + tmp=$(mktemp) + { echo "$IMPORT_LINE"; cat "$CLAUDEMD"; } > "$tmp" + mv "$tmp" "$CLAUDEMD" +fi +``` + +This pattern: +- Creates the file if missing (AWARE-01) +- Checks only the first line for the import (D-07, D-08) +- Prepends without destroying existing content (D-03) +- Uses `grep -qF` for fixed-string match (no regex needed) +- Uses mktemp + mv for atomic write (established pattern in codebase) + +### Anti-Patterns to Avoid + +- **Inline sed -i for prepending:** Non-portable, and the script already uses the mktemp+mv pattern for gitconfig. Stay consistent. +- **Writing SANDBOX.md content inside CLAUDE.md:** Defeats the purpose of the two-file approach. SANDBOX.md is overwritten every launch; CLAUDE.md is user-owned. +- **Variable expansion in SANDBOX.md heredoc:** Would break if user's env has unexpected values. Use single-quoted delimiter. + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Atomic file write | Custom lock files | `mktemp` + `mv` | Already the pattern in codebase (gitconfig), atomic on same filesystem | +| First-line check | Complex parsing | `head -1 \| grep -qF` | One-liner, POSIX-compatible, no edge cases for this use | + +## Common Pitfalls + +### Pitfall 1: Heredoc Indentation +**What goes wrong:** Using `<<-` with tabs for indentation but the content has mixed tabs/spaces, producing wrong output. +**Why it happens:** `<<-` only strips leading tabs, not spaces. +**How to avoid:** Use `<<` (no dash) with no indentation in the heredoc body. The SANDBOX.md content should be flush-left in the script. +**Warning signs:** SANDBOX.md has unexpected leading whitespace. + +### Pitfall 2: Import Line Getting Duplicated +**What goes wrong:** If the check logic has a bug, `@SANDBOX.md` could appear multiple times in CLAUDE.md. +**Why it happens:** Checking for the line anywhere in the file instead of just line 1, or not checking at all. +**How to avoid:** Always check `head -1` specifically. The import must be on line 1 for Claude Code to process it before other content. +**Warning signs:** Multiple `@SANDBOX.md` lines in CLAUDE.md after several launches. + +### Pitfall 3: CLAUDE.md Import Approval Dialog +**What goes wrong:** Claude Code shows an approval dialog for the `@SANDBOX.md` import, breaking the "zero friction" goal. +**Why it happens:** Claude Code may prompt for approval when it encounters external imports for the first time. +**How to avoid:** The official docs state this dialog appears for "external imports in a project" -- user-level `~/.claude/CLAUDE.md` imports may behave differently since they are user-owned. This needs testing. If the dialog appears, it's a one-time approval. +**Warning signs:** User sees "approve imports" prompt on first claudebox session. +**Risk level:** LOW -- even if it appears, it's one-time and self-explanatory. + +### Pitfall 4: Race Condition with Temp File +**What goes wrong:** If claudebox is killed between writing the temp file and mv, a stale temp file is left behind. +**Why it happens:** No cleanup trap for this specific temp file. +**How to avoid:** The existing trap at line 109 cleans up `$GITCONFIG_TMP`. Either extend that trap or accept that orphan temp files in /tmp are harmless (cleaned on reboot). +**Warning signs:** None in practice -- /tmp is ephemeral. + +## Code Examples + +### SANDBOX.md Content Structure + +Based on decisions D-04, D-05, D-06, the content should follow this structure. Exact prose is Claude's discretion. + +```markdown +# Sandbox Environment + +You are running inside a bubblewrap (bwrap) sandbox managed by claudebox. +Your filesystem is isolated -- only the current working directory and +essential system paths are mounted. + +## Installing Tools + +You have two ways to install tools on the fly: + +**Comma (preferred for quick one-off commands):** +`, ripgrep` runs ripgrep without permanent installation. Comma uses +nix-index to find the right package automatically. + +**Nix shell (for persistent access within the session):** +`nix shell nixpkgs#python3 -c python3 script.py` runs a command with +a package available. To keep it in your PATH for the session: +`nix shell nixpkgs#python3` then use `python3` normally. + +## Default Restrictions + +By default, the following are not mounted into the sandbox: +- SSH keys (~/.ssh) +- GPG and age keys (~/.gnupg, age key files) +- Cloud credentials (~/.aws, ~/.config/gcloud) +- Tailscale state + +If your setup has been customized, some of these may be available. + +## Git + +Your git identity (name and email) is pre-configured from the host. +The `safe.directory` setting trusts the mounted working directory. +For remote operations, prefer HTTPS URLs over SSH since SSH keys +are not available by default. +``` + +### Shell Implementation + +```bash +# === Sandbox-aware prompting (AWARE-01, AWARE-02) === + +# Write SANDBOX.md -- fully managed, overwritten every launch (D-02) +cat > "$HOME/.claudebox/SANDBOX.md" << 'SANDBOXEOF' +[content here] +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 +``` + +Note: Using `[[ "$(head -1 ...)" != "@SANDBOX.md" ]]` is simpler and avoids needing grep. Exact string comparison on the first line. [VERIFIED: standard bash string comparison] + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| Monolithic CLAUDE.md | `@import` syntax for modular files | Claude Code ~1.0 (2025) | Allows splitting managed vs user-owned content cleanly | +| `--append-system-prompt` | `~/.claude/CLAUDE.md` + `@imports` | Claude Code memory system | File-based approach persists across sessions without CLI flags | + +**Claude Code `@import` behavior (verified):** +- Relative paths resolve relative to the containing file [CITED: code.claude.com/docs/en/memory] +- Maximum import depth: 5 hops [CITED: code.claude.com/docs/en/memory] +- User-level `~/.claude/CLAUDE.md` is loaded at session start for all projects [CITED: code.claude.com/docs/en/memory] +- First 200 lines or 25KB of MEMORY.md is the auto-memory limit, but CLAUDE.md files load in full [CITED: code.claude.com/docs/en/memory] + +## Assumptions Log + +| # | Claim | Section | Risk if Wrong | +|---|-------|---------|---------------| +| A1 | `@SANDBOX.md` in `~/.claude/CLAUDE.md` resolves to `~/.claude/SANDBOX.md` without needing `./` prefix | Architecture Patterns | Import silently fails, Claude doesn't see sandbox context. Mitigation: test on first run. | +| A2 | User-level `~/.claude/CLAUDE.md` imports don't trigger an approval dialog | Pitfall 3 | One-time dialog appears. Low impact -- user approves once. | + +## Open Questions + +1. **Does `@SANDBOX.md` (no path prefix) resolve correctly from `~/.claude/CLAUDE.md`?** + - What we know: Official docs say relative paths resolve relative to the containing file. The simplest form `@filename` should resolve to the same directory. + - What's unclear: Known bugs exist with tilde-expansion paths (`@~/.claude/foo.md`), but our case is simpler (same-directory, no tilde). + - Recommendation: Test during implementation. If it fails, try `@./SANDBOX.md` as fallback. + +2. **Comment marker for the import line?** + - What we know: D-03 says claudebox only touches the first line. A comment marker could help identify it as managed. + - What's unclear: Whether adding a comment on the same line as `@SANDBOX.md` breaks the import. + - Recommendation: Keep the import line bare (`@SANDBOX.md` only). Add a comment on line 2 if desired: ``. + +## Sources + +### Primary (HIGH confidence) +- [Claude Code Memory Documentation](https://code.claude.com/docs/en/memory) -- `@import` syntax, resolution rules, file loading order, user-level CLAUDE.md behavior +- `claudebox.sh` (current codebase) -- existing patterns, integration points, line numbers + +### Secondary (MEDIUM confidence) +- [GitHub Issue #4754](https://github.com/anthropics/claude-code/issues/4754) -- relative path resolution bug (old version, different case than ours) +- [GitHub Issue #8765](https://github.com/anthropics/claude-code/issues/8765) -- tilde expansion bug (different from our same-directory case) + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH -- no new dependencies, pure shell scripting +- Architecture: HIGH -- integration point is clear, patterns established in codebase +- Pitfalls: HIGH -- well-understood shell patterns, main risk is Claude Code import behavior (low consequence) + +**Research date:** 2026-04-09 +**Valid until:** 2026-05-09 (Claude Code import syntax is stable)