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