claudebox/.planning/phases/03-sandbox-aware-prompting/03-RESEARCH.md
Christopher Mühl c5e8cca867 feat(05-01): rewrite mount architecture with per-project instance isolation
- Replace --bind ~/.claudebox + --symlink with direct --bind ~/.claude ~/.claude
- Add compute_canonical_root() function using git rev-parse --git-common-dir
- Add per-project INSTANCE_DIR via sha256sum[:16] of canonical git root
- Overlay projects/ with per-project hash dir for isolated conversation history
- Overlay history.jsonl and SANDBOX.md as file-level bind mounts
- Update credential mount target from ~/.claudebox to ~/.claude
- Add CLAUDE_JSON_FILE (~/.claude.json) detection and conditional bind mount
- Remove stale CLAUDE.md injection logic (D-06: user's real CLAUDE.md used)
- Update dry-run block and print_audit to reflect new mount layout
- Update SANDBOX.md heredoc to remove ~/.claudebox reference
2026-04-13 09:00:53 +00:00

15 KiB

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>

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

</user_constraints>

<phase_requirements>

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

</phase_requirements>

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.

# 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

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

# 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

# === 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: <!-- managed by claudebox -->.

Sources

Primary (HIGH confidence)

  • Claude Code Memory Documentation -- @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)

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)