docs(03): research sandbox-aware prompting
This commit is contained in:
parent
2ee1588408
commit
351dcc5c8b
1 changed files with 280 additions and 0 deletions
280
.planning/phases/03-sandbox-aware-prompting/03-RESEARCH.md
Normal file
280
.planning/phases/03-sandbox-aware-prompting/03-RESEARCH.md
Normal file
|
|
@ -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>
|
||||
|
||||
## 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)
|
||||
Loading…
Add table
Reference in a new issue