claudebox/.planning/phases/03-sandbox-aware-prompting/03-RESEARCH.md

280 lines
15 KiB
Markdown

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