197 lines
15 KiB
Markdown
197 lines
15 KiB
Markdown
# 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:
|
|
|
|
```sh
|
|
# ~/.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
|
|
|
|
- [Claude Code Authentication Docs](https://code.claude.com/docs/en/authentication) — auth file locations (HIGH confidence)
|
|
- [inventivehq.com Claude Code config guide](https://inventivehq.com/knowledge-base/claude/where-configuration-files-are-stored) — file list verification (MEDIUM confidence)
|
|
- [Claude Code .credentials.json bug issue #1414](https://github.com/anthropics/claude-code/issues/1414) — confirms Linux credential file path (HIGH confidence)
|
|
- [milvus.io Claude Code local storage](https://milvus.io/blog/why-claude-code-feels-so-stable-a-developers-deep-dive-into-its-local-storage-design.md) — project dir encoding, storage layout (MEDIUM confidence)
|
|
- [slirp4netns README](https://github.com/rootless-containers/slirp4netns) — network isolation mechanics, --disable-host-loopback (HIGH confidence)
|
|
- [slirp4netns man page](https://github.com/rootless-containers/slirp4netns/blob/master/slirp4netns.1.md) — --disable-host-loopback details (HIGH confidence)
|
|
- [bubblewrap issue #392](https://github.com/containers/bubblewrap/issues/392) — slirp4netns+bwrap integration status (not merged into bwrap; external process approach needed) (HIGH confidence)
|
|
- [bwrap man page](https://manpages.debian.org/unstable/bubblewrap/bwrap.1.en.html) — --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*
|