claudebox/.planning/research/FEATURES.md

15 KiB

Feature Landscape

Domain: CLI sandbox wrapper (Nix/bubblewrap) for AI coding agents Researched: 2026-04-10 (v2.0 milestone update — network isolation, profiles, auth passthrough) Confidence: MEDIUM-HIGH (web-verified for auth files, slirp4netns mechanics, Claude Code storage layout; MEDIUM for devshell injection patterns)


v1.0 Features (Already Built — Reference Only)

Core sandbox, env allowlist, secret path hiding, minimal PATH, pre-launch audit, comma/nix tool provisioning, SANDBOX.md injection, --check/--dry-run/--shell modes. Do not re-implement.


v2.0 New Features Under Research

Table Stakes for v2.0

Features required for the milestone to be considered complete. Without these, the milestone goals are unmet.

Feature Why Expected Complexity Notes
Host auth passthrough Without it, every sandbox launch requires re-authenticating Claude Code — subscriptions and API keys are unusable LOW Mount ~/.claude/.credentials.json read-only into ~/.claudebox/.credentials.json. On Linux, Claude stores auth in ~/.claude/.credentials.json (mode 0600). A read-only bind mount passes the file into the sandbox; Claude Code reads it on startup. No write access needed — token refresh writes back to the same file, so the mount must permit writes or use a copy-on-launch approach.
Per-project instance isolation Running claudebox in two different projects should not share conversation history, todos, or project settings. Currently both projects write to the same ~/.claudebox/projects/ keyed by encoded path — works but muddles state if ~/.claudebox is wiped or shared LOW-MEDIUM Claude Code stores sessions at ~/.claude/projects/{path-encoded}/. A per-project instance dir ~/.claudebox/instances/<hash>/ contains its own .claude/; bind-mount that instead of ~/.claudebox directly. Hash = SHA1 or MD5 of CWD absolute path. Instance dir auto-created on first launch.
Named profiles Users need repeatable configurations for different types of work (e.g., "web" profile with cloud creds, "offline" profile with no network) MEDIUM Profile = named config at ~/.claudebox/profiles/<name>.conf (or .toml). Contains: extra env vars to pass, extra mount paths (ro or rw), extra Nix packages, network tier. Activated via --profile foo or CLAUDEBOX_PROFILE=foo. Merges with defaults; profile settings additive on top of base config.
Tiered network isolation "full" (current, no restriction), "internet-only" (blocks LAN/Tailscale/localhost), "none" (fully offline) serve distinct use cases: internet-only prevents Claude from reaching internal services; none is for sensitive codebases HIGH Three tiers: full = current behavior (host network shared). none = --unshare-net in bwrap, no connectivity. internet-only = --unshare-net + slirp4netns as separate process providing NAT'd internet via TAP device (--disable-host-loopback to block LAN). This is the complex tier.
Nix devshell injection Projects with flake.nix defining a devShell should be able to make those packages available to Claude inside the sandbox MEDIUM-HIGH Two viable approaches: (a) resolve devShell's buildInputs to store paths and add them to PATH/mounts before launch; (b) run nix print-dev-env to get env vars and merge them. Approach (b) is cleaner — nix print-dev-env .#devShell emits JSON of env vars including PATH modifications; parse and forward into sandbox ENV_ARGS.

Differentiators for v2.0

Features that go beyond minimum requirements and add meaningful value.

Feature Value Proposition Complexity Notes
Profile inheritance / extends A "work" profile that extends "default" instead of rewriting it avoids duplication and drift LOW Simple key in profile config: extends = "default". Load base profile first, merge overrides. Two levels sufficient; no deep inheritance chains needed.
Network tier shown in env audit User sees "Network: internet-only" in pre-launch audit alongside env vars LOW Extend existing audit display to include active profile name and network tier. Auditability is the core UX principle of claudebox.
Instance dir auto-cleanup Old instance dirs from deleted projects waste disk space over time LOW Add claudebox --gc that removes instance dirs for paths that no longer exist on disk.
Profile --list and --show Discoverability of what profiles exist and what they contain LOW claudebox --list-profiles and claudebox --show-profile foo. Text output, no interactive UX needed.
nix print-dev-env opt-in flag Devshell injection requires nix eval which can be slow; should be explicit opt-in not auto LOW --devshell flag or devshell = true in profile. Never run nix print-dev-env implicitly.

Anti-Features for v2.0

Features that seem natural extensions but should be explicitly avoided.

Anti-Feature Why Avoid What to Do Instead
Domain-level network allowlists Already declared out of scope in PROJECT.md. slirp4netns can be combined with iptables for domain filtering, but complexity is extreme and maintenance burden is high Three tiers (full/internet-only/none) cover the actual use cases
Writable auth file mounts Mounting .credentials.json read-write means Claude could overwrite or corrupt tokens Mount read-only; Claude Code reads tokens, does not need to write them during a session (token refresh path is rare and can be handled by relaunching)
Auto-detect and inject all devShell vars Running nix print-dev-env silently on every launch for any project with a flake.nix breaks the "no surprises" principle and is slow Explicit --devshell flag only
Profile passwords / encryption Profiles contain env var names, mount paths, package names — not secret values. API keys still come from host env via allowlist Keep profile files plaintext; they don't need to store secrets
Per-profile CLAUDE.md injection Profiles should not override sandbox system prompt — that breaks the security invariant that Claude always knows it's sandboxed SANDBOX.md injection is unconditional; profiles can only add mounts/env/packages/network
Multi-profile activation "Merge profile A and profile B" creates order-dependent ambiguity One active profile per launch. Use extends for composition.
Automatic profile detection from project Detecting profile from .claudebox.toml in project dir requires trusting project-local config, which is a sandbox escape vector Profiles live in ~/.claudebox/profiles/ only. User chooses profile at launch.

Feature Dependencies (v2.0)

Per-project instance isolation
    └──requires──> Auth passthrough works correctly
                   (auth must be in instance dir or globally available)

Named profiles
    └──provides──> Network tier configuration
    └──provides──> Extra env vars (additive to base allowlist)
    └──provides──> Extra mount paths
    └──provides──> Nix package list for PATH injection

Tiered network isolation
    └──requires──> "none" tier: just --unshare-net (trivial, no deps)
    └──requires──> "internet-only" tier: slirp4netns binary + process management
                   └──requires──> slirp4netns in runtimeInputs
                   └──requires──> pid-file or fd-passing to connect slirp4netns to bwrap net ns

Nix devshell injection
    └──requires──> `nix print-dev-env` available inside pre-launch environment
    └──requires──> CWD has a flake.nix (validated before attempting)
    └──enhances──> Named profiles (devshell can be a profile option)

Profile inheritance (extends)
    └──requires──> Named profiles exist first

Dependency Notes

  • Auth passthrough and instance isolation interact: If each instance dir is fully self-contained, auth files must either be in the instance dir (copied/linked from ~/.claude) or mounted globally. The cleanest approach: mount ~/.claude/.credentials.json read-only directly into ~/.claudebox/.credentials.json unconditionally, keep it separate from instance isolation (which only scopes project history and todos).

  • internet-only tier is a process management problem: slirp4netns must be started before bwrap, connected to bwrap's network namespace via the --netns-type=path approach or by passing the network namespace fd. bwrap's --userns-block-fd / --info-fd / --json-status-fd flags provide synchronization primitives for this. This is the highest-complexity feature in the milestone.

  • Nix devshell injection does not require profile system: It can be an independent --devshell flag. Profile system can optionally set devshell = true. The two features are parallel, not sequential.


Implementation Complexity Ranking

Ordered from lowest to highest complexity, within the v2.0 scope:

Feature Complexity Key Challenge
Auth passthrough LOW Identify correct files; decide ro vs rw; one-time mount addition
Network tier: none LOW Add --unshare-net to bwrap call when tier=none
Profile --list/--show LOW File glob + pretty-print
Network tier in audit display LOW Extend existing audit code
Per-project instance isolation LOW-MEDIUM Hash CWD, mkdir, adjust bind mount target
Named profiles (parsing + merge) MEDIUM Config file format, merge logic, validation
Nix devshell injection MEDIUM nix print-dev-env output parsing, env merge, PATH extension
Network tier: internet-only HIGH slirp4netns process lifecycle, bwrap netns synchronization, resolv.conf injection

Auth Passthrough: Key Findings

Claude Code on Linux stores auth at ~/.claude/.credentials.json (mode 0600). This file contains OAuth tokens. It is the only file strictly required for subscription/API key auth to work. (Source: inventivehq.com Claude Code configuration guide, verified 2026-04-10.)

Mount strategy:

  • --ro-bind "$HOME/.claude/.credentials.json" "$HOME/.claudebox/.credentials.json" — read-only passthrough
  • If file does not exist (user not authenticated), skip the mount silently; Claude Code will prompt for auth at first launch and write to the instance dir
  • settings.json and settings.local.json in ~/.claude/ contain user preferences — mounting these read-only is optional but reduces friction (user's saved settings carry over)

Per-Project Instance Isolation: Key Findings

Claude Code stores per-project data at ~/.claude/projects/{encoded-path}/ where encoding replaces / and . with -. (Source: milvus.io Claude Code local storage deep-dive, verified 2026-04-10.) Per-project instance dirs in claudebox would contain their own ~/.claude/-equivalent, keeping conversation history, todos, and shell snapshots scoped per project.

Instance dir path: ~/.claudebox/instances/$(echo -n "$CWD" | sha256sum | cut -c1-16)/

The bind-mount that currently maps ~/.claudebox~/.claude inside the sandbox would instead map ~/.claudebox/instances/<hash>~/.claude. Auth files should NOT live inside instance dirs (they are cross-project); they should be mounted separately.


Tiered Network Isolation: Key Findings

Tier: none--unshare-net added to bwrap flags. No other changes. Claude Code cannot reach any network. DNS fails. Nix store is local so tool installation via comma still works.

Tier: internet-only — Requires slirp4netns (in nixpkgs as pkgs.slirp4netns). The pattern:

  1. bwrap is invoked with --unshare-net plus --info-fd 4 to emit the sandbox PID
  2. A wrapper script reads the PID from the info fd
  3. slirp4netns is started: slirp4netns --configure --disable-host-loopback <pid> tap0
  4. --disable-host-loopback prevents access to host's 127.x.x.x, 10.x.x.x (LAN), and Tailscale addresses
  5. A custom resolv.conf pointing to slirp4netns's built-in DNS (10.0.2.3) is bind-mounted

Important: bwrap's --info-fd is a synchronization mechanism — bwrap blocks until the info fd is closed, allowing the wrapper to set up the network namespace before Claude Code starts. (Source: bwrap man page, slirp4netns GitHub README, verified 2026-04-10.)

slirp4netns does not require root privileges. It is in nixpkgs and works on NixOS.

Tier: full — Current behavior, no changes.


Named Profiles: Design Recommendation

Config format: plain shell-style or TOML. Given the project is pure shell script with no TOML parser available in runtimeInputs, shell key=value format (like .env files) is simplest:

# ~/.claudebox/profiles/web.conf
network=internet-only
devshell=false
env=MY_EXTRA_VAR,ANOTHER_VAR
mount_ro=/home/user/shared-docs
packages=awscli2 kubectl
extends=default

Parsing: source-able if shell syntax, or simple grep/awk for key=value. Shell source is dangerous (arbitrary code execution) — use awk -F= '{print $1, $2}' parser instead.


Nix Devshell Injection: Design Recommendation

nix print-dev-env --json .#devShell (or just . for default devShell) outputs a JSON object with all environment variables the devShell would set, including a modified PATH. Parsing this with jq (already in runtimeInputs) and merging into ENV_ARGS is the right approach.

Caveats:

  • nix print-dev-env runs Nix evaluation — can be slow (2-10s) for complex flakes
  • Must be run from CWD before entering sandbox
  • Not all devShell vars are safe to forward (some reference host paths outside /nix/store)
  • Should filter to only PATH modifications and explicitly safe vars

Sources


Feature research updated for v2.0 milestone: network isolation, profiles, auth passthrough, instance isolation, devshell injection Researched: 2026-04-10