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.jsonread-only directly into~/.claudebox/.credentials.jsonunconditionally, 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=pathapproach or by passing the network namespace fd. bwrap's--userns-block-fd/--info-fd/--json-status-fdflags 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
--devshellflag. Profile system can optionally setdevshell = 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.jsonandsettings.local.jsonin~/.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:
- bwrap is invoked with
--unshare-netplus--info-fd 4to emit the sandbox PID - A wrapper script reads the PID from the info fd
- slirp4netns is started:
slirp4netns --configure --disable-host-loopback <pid> tap0 --disable-host-loopbackprevents access to host's 127.x.x.x, 10.x.x.x (LAN), and Tailscale addresses- A custom
resolv.confpointing 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-envruns 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
- Claude Code Authentication Docs — auth file locations (HIGH confidence)
- inventivehq.com Claude Code config guide — file list verification (MEDIUM confidence)
- Claude Code .credentials.json bug issue #1414 — confirms Linux credential file path (HIGH confidence)
- milvus.io Claude Code local storage — project dir encoding, storage layout (MEDIUM confidence)
- slirp4netns README — network isolation mechanics, --disable-host-loopback (HIGH confidence)
- slirp4netns man page — --disable-host-loopback details (HIGH confidence)
- bubblewrap issue #392 — slirp4netns+bwrap integration status (not merged into bwrap; external process approach needed) (HIGH confidence)
- bwrap man page — --info-fd synchronization mechanism (HIGH confidence)
- Training data: nix print-dev-env JSON output format, jq availability (MEDIUM confidence — verify exact flags)
Feature research updated for v2.0 milestone: network isolation, profiles, auth passthrough, instance isolation, devshell injection Researched: 2026-04-10