16 KiB
Technology Stack
Project: claudebox v2.0 — Network Isolation & Profiles Researched: 2026-04-10 Confidence: HIGH (network isolation), HIGH (auth passthrough), MEDIUM (profile config format), MEDIUM (devshell injection)
Scope: NEW additions only. Existing validated stack (writeShellApplication, bubblewrap, comma-with-db, coreutils/git/curl/jq/ripgrep/fd/nix/nodejs) is carried forward unchanged.
New Runtime Dependencies
Add to runtimeInputs / runtimeDeps
| Package | Nixpkgs Name | Version | Purpose | Why | Confidence |
|---|---|---|---|---|---|
slirp4netns |
pkgs.slirp4netns |
1.3.3 (June 2025) | User-mode networking for internet-only tier | Only tool enabling unprivileged network namespace → internet without root. Creates TAP device in bwrap's --unshare-net namespace and routes traffic through host userspace. |
HIGH |
util-linux (unshare) |
pkgs.util-linux |
current nixpkgs | Namespace coordination helper | unshare is used internally for PID capture; nsenter may be needed. Most is covered by coreutils but unshare is in util-linux. Actually bwrap handles namespace creation itself — only needed if you need readlink or nsenter beyond coreutils. Defer unless needed. |
MEDIUM |
Note on slirp4netns architecture: slirp4netns must run on the host side and be given the PID of the bwrap'd process's network namespace. It cannot run inside the sandbox. This means slirp4netns must be in runtimeInputs so it is available in the wrapper script's PATH pre-bwrap exec.
No new Nix flake inputs needed — pkgs.slirp4netns is in nixpkgs unstable.
Network Isolation: Tiered Architecture
Three Tiers
| Tier | bwrap Flag | slirp4netns | Effect |
|---|---|---|---|
full (default) |
(no change) | Not used | Full host network access. Current v1.0 behavior. |
internet |
--unshare-net |
Yes, with --disable-host-loopback |
Internet access via NAT. No LAN, no Tailscale, no localhost. |
none |
--unshare-net |
Not used | Fully offline. Loopback only (localhost works inside sandbox). |
Internet Tier: The Shell Pattern
The internet tier requires process coordination because slirp4netns runs on the host and must be given the bwrap child's PID. exec bwrap cannot be used — you need the PID. The pattern:
# Create a ready pipe for synchronization
ready_fd_r ready_fd_w
exec {ready_fd_r}<> <(:) # or use mkfifo
# Launch bwrap in background to get its PID
bwrap \
--unshare-net \
--info-fd 4 \ # bwrap writes JSON with child PID to this FD
... \
-- "$CLAUDE_BIN" ... &
BWRAP_PID=$!
# Read child PID from bwrap's --info-fd output
# (bwrap writes {"child-pid": N} to fd 4)
read -r CHILD_PID < <(...)
# Start slirp4netns targeting that PID's network namespace
slirp4netns \
--configure \
--mtu=65520 \
--disable-host-loopback \
--ready-fd 5 \ # slirp4netns signals ready on this FD
"$CHILD_PID" tap0 &
SLIRP_PID=$!
# Wait for slirp4netns to signal ready
read -r _ <&5
# Now wait for bwrap to finish
wait "$BWRAP_PID"
EXIT_CODE=$?
# Clean up slirp4netns
kill "$SLIRP_PID" 2>/dev/null
exit "$EXIT_CODE"
bwrap --info-fd flag: bwrap writes {"child-pid": N} (JSON) to the specified file descriptor when the child starts. This is the correct way to get the sandboxed process's PID for slirp4netns targeting. This avoids the two-terminal pattern seen in general slirp4netns docs.
slirp4netns --ready-fd flag: slirp4netns writes "1" to this FD when TAP interface is configured. Use this to know when the network is ready before proceeding.
slirp4netns default network config inside sandbox: 10.0.2.100 (host IP), 10.0.2.2 (gateway), 10.0.2.3 (DNS). These are hardcoded defaults that work for most cases.
--disable-host-loopback: Prevents the sandbox from connecting to 127.0.0.1 on the host side. This is the key flag for blocking LAN/Tailscale services. Combined with a new network namespace, the sandbox cannot see host LAN addresses (192.168.x.x, 100.x.x.x Tailscale ranges) because it has no routes to them.
Confidence: HIGH for the approach. MEDIUM for exact bash FD plumbing — test the --info-fd JSON parsing.
None Tier: Simple
bwrap --unshare-net ... -- "$CLAUDE_BIN" ...
No slirp4netns. bwrap creates an isolated network namespace with only loopback (127.0.0.1) configured. exec bwrap still works here.
Full Tier: No Change
Current behavior. No --unshare-net. exec bwrap still works.
Network Tier Implication: exec vs wait
For full and none tiers, exec bwrap is clean and efficient. For the internet tier, you cannot use exec because you need the PID. The wrapper script must branch:
if [[ "$NETWORK_TIER" == "internet" ]]; then
# background + slirp4netns + wait pattern
else
exec bwrap ... -- "$SANDBOX_CMD"
fi
Auth Passthrough: Host ~/.claude Files
What Claude Code Stores in ~/.claude
From official docs (code.claude.com/docs/en/authentication):
~/.claude/.credentials.json— OAuth tokens for Claude.ai subscription (Linux, mode 0600)~/.claude/settings.json— User settings (model, editor preferences, etc.)$CLAUDE_CONFIG_DIR/.credentials.json— If env var is set
Auth passthrough means: Mount host ~/.claude/.credentials.json read-only into the sandbox's ~/.claude/ directory so Claude Code inside can authenticate with the user's existing subscription without re-logging in.
Current Situation vs Target
Currently, ~/.claudebox is bind-mounted as ~/.claude inside the sandbox. This correctly isolates conversation history per the existing design. The problem: ~/.claudebox does not have credentials — those live in host's ~/.claude.
Recommended Pattern
Selective file mounts, not directory replacement:
# Mount instance dir as ~/.claude (for history isolation)
--bind "$INSTANCE_DIR" "$HOME/.claude" \
# Then overlay specific auth files from host ~/.claude (read-only)
--ro-bind "$HOME/.claude/.credentials.json" "$HOME/.claude/.credentials.json" \
Mount ordering in bwrap: the --ro-bind overlay after the --bind for the directory works because bwrap processes mounts sequentially. The credentials file mount overlays the one that would exist in the instance dir.
Guard the mount: Only add --ro-bind if the credentials file exists on the host:
AUTH_MOUNTS=()
if [[ -f "$HOME/.claude/.credentials.json" ]]; then
AUTH_MOUNTS+=(--ro-bind "$HOME/.claude/.credentials.json" "$HOME/.claude/.credentials.json")
fi
Confidence: HIGH for the pattern. The file path is confirmed from official docs.
Per-Project Instance Isolation
Instance Directory Pattern
~/.claudebox/instances/<hash>/.claude/
Where <hash> is a stable identifier for the project. Options:
| Hash Input | Pros | Cons |
|---|---|---|
md5sum "$CWD" |
Stable as long as project doesn't move | Breaks if project relocated |
sha256sum "$CWD" |
Same | Same |
basename "$CWD" |
Human readable | Collides across different projects with same dir name |
basename "$CWD"-md5sum "$CWD" | head -c8 |
Human readable + unique | Slightly longer |
Recommendation: $(basename "$CWD")-$(echo "$CWD" | md5sum | head -c8) — readable in ls output, collision-resistant.
INSTANCE_HASH="$(basename "$CWD")-$(echo "$CWD" | md5sum | cut -c1-8)"
INSTANCE_DIR="$HOME/.claudebox/instances/$INSTANCE_HASH"
mkdir -p "$INSTANCE_DIR"
# In bwrap call:
--bind "$INSTANCE_DIR" "$HOME/.claude" \
No new packages needed — md5sum is in coreutils (already in runtimeInputs).
Confidence: HIGH for the pattern.
Named Profiles: Config Format
Recommendation: Bash-sourced config files (.sh)
Format: Simple KEY=value bash files, sourced by the wrapper script.
# ~/.claudebox/profiles/myproject.sh
CLAUDEBOX_NETWORK=internet
CLAUDEBOX_EXTRA_ENV=MY_API_KEY,ANOTHER_VAR
CLAUDEBOX_EXTRA_MOUNTS="/data/shared:/data/shared:ro"
CLAUDEBOX_EXTRA_PACKAGES="python3 postgresql"
Why bash-sourced, not TOML/JSON/YAML:
- Zero new dependencies — no parser needed, no
jqgymnastics for complex formats - Fits the existing stack (
bash,coreutilsalready in PATH) - Consistent with how the project already handles
CLAUDEBOX_EXTRA_ENV(existing escape hatch) writeShellApplication+ shellcheck already validates the wrapper; sourcing a profile file is idiomatic bash- Security: the profile file runs as the user who owns it (
~/.claudebox/profiles/is user-owned), same attack surface as.bashrc
Why not TOML:
- Requires a TOML parser.
jqcannot parse TOML. Options would bedasel,taplo, orpython3 -c "import tomllib"— all add dependencies or requirenix shellat startup (unacceptable latency). - OpenAI Codex uses TOML but they have a full Rust binary with TOML parsing built in. claudebox is a shell script.
Why not JSON:
jqis already in PATH and could parse JSON. But JSON does not support comments, which makes profiles harder to self-document. Multi-value fields (extra mounts list) are awkward as JSON arrays being bash-parsed.
Profile loading:
# Resolve profile path
if [[ -n "${CLAUDEBOX_PROFILE:-}" ]]; then
PROFILE_FILE="$HOME/.claudebox/profiles/${CLAUDEBOX_PROFILE}.sh"
if [[ -f "$PROFILE_FILE" ]]; then
# shellcheck source=/dev/null
source "$PROFILE_FILE"
else
echo "Error: profile '$CLAUDEBOX_PROFILE' not found at $PROFILE_FILE" >&2
exit 1
fi
fi
CLI flag: --profile foo sets CLAUDEBOX_PROFILE=foo before the source call.
Confidence: HIGH for the bash-sourced approach given the project constraints.
Nix Devshell Injection Per Profile
The Goal
A profile can declare extra Nix packages to make available inside the sandbox. Example: a Python project profile adds python3, poetry to the sandbox PATH without hardcoding them into the global claudebox derivation.
Approach: nix shell to Resolve Store Paths at Runtime
The derivation cannot know profile packages at build time. Resolution happens at wrapper script execution time.
Pattern:
# In profile file:
CLAUDEBOX_EXTRA_PACKAGES="python3 poetry"
# In wrapper script, if CLAUDEBOX_EXTRA_PACKAGES is set:
if [[ -n "${CLAUDEBOX_EXTRA_PACKAGES:-}" ]]; then
# Build a space-separated list of nixpkgs#pkg references
PKG_ARGS=()
read -ra pkgs <<< "$CLAUDEBOX_EXTRA_PACKAGES"
for pkg in "${pkgs[@]}"; do
PKG_ARGS+=("nixpkgs#$pkg")
done
# Resolve store paths using nix path-info
EXTRA_PATHS=""
for pkg in "${pkgs[@]}"; do
store_path=$(nix eval --raw "nixpkgs#${pkg}.outPath" 2>/dev/null)
if [[ -n "$store_path" ]]; then
EXTRA_PATHS="${EXTRA_PATHS}:${store_path}/bin"
fi
done
# Prepend to SANDBOX_PATH
SANDBOX_PATH="${EXTRA_PATHS#:}:${SANDBOX_PATH}"
fi
Alternative: nix build --no-link --print-out-paths
store_path=$(nix build --no-link --print-out-paths "nixpkgs#$pkg" 2>/dev/null)
This forces the package to be built/fetched if not in store. nix eval --raw does not build — it only resolves the path, which may not exist if the package isn't already in the store. For a wrapper script, nix build is safer but adds latency on first use.
Recommendation: Use nix build --no-link --print-out-paths "nixpkgs#${pkg}" per package. Cache the result is implicit (Nix store is content-addressed; rebuild is a no-op if already present).
No new Nix flake inputs needed — nix is already in runtimeInputs.
Confidence: MEDIUM — nix eval --raw vs nix build behavior distinction should be verified. The pattern is sound; exact subcommand flags may need adjustment.
Extra Mount Injection Per Profile
Profile-declared extra mounts follow the same additive pattern:
# In profile:
CLAUDEBOX_EXTRA_MOUNTS="/data/shared:/data/shared:ro /home/user/keys:/keys:ro"
# In wrapper:
EXTRA_MOUNT_ARGS=()
if [[ -n "${CLAUDEBOX_EXTRA_MOUNTS:-}" ]]; then
read -ra mount_specs <<< "$CLAUDEBOX_EXTRA_MOUNTS"
for spec in "${mount_specs[@]}"; do
IFS=':' read -r src dst mode <<< "$spec"
if [[ "$mode" == "ro" ]]; then
EXTRA_MOUNT_ARGS+=(--ro-bind "$src" "$dst")
else
EXTRA_MOUNT_ARGS+=(--bind "$src" "$dst")
fi
done
fi
Confidence: HIGH — straightforward bwrap flag construction.
Alternatives Considered
| Feature | Recommended | Alternative | Why Not |
|---|---|---|---|
| Network isolation | slirp4netns + --unshare-net |
pasta (passt) | pasta is newer and used by podman but less mature than slirp4netns for bwrap integration; slirp4netns has --ready-fd for synchronization |
| Network isolation | slirp4netns + --unshare-net |
nftables/iptables rules | Requires root; incompatible with unprivileged bwrap model |
| Profile format | bash-sourced .sh |
TOML | TOML requires a parser binary not in the current stack |
| Profile format | bash-sourced .sh |
JSON + jq | JSON lacks comments; multi-value fields are awkward in bash; .sh is simpler |
| Devshell injection | nix build --no-link --print-out-paths |
Include packages in derivation | Derivation is built once; profiles are dynamic; runtime resolution is the only option |
| Instance hash | basename-md5sum |
git hash | Not all projects are git repos; CWD is always available |
What NOT to Add
| Avoid | Why | Use Instead |
|---|---|---|
pasta / passt |
More complex setup than slirp4netns; slirp4netns is the established tool in nixpkgs with --ready-fd sync support |
slirp4netns |
iptables / nftables for network filtering |
Requires root; incompatible with unprivileged bwrap model | --unshare-net + slirp4netns |
dasel / taplo for TOML parsing |
Adds dependencies not in current stack | Bash-sourced profile files |
docker / podman for extra isolation |
Project constraint: bwrap only, no Docker | Existing bwrap model |
| Separate Nix flake per profile | Each profile would need a separate derivation build; defeats the purpose of dynamic profiles | Runtime nix build resolution |
CLAUDE_CONFIG_DIR env var redirection |
Complicates the ~/.claude → instance dir mapping; better to mount directly |
Direct --bind "$INSTANCE_DIR" "$HOME/.claude" |
Version Verification Checklist
Before implementation:
- Verify
pkgs.slirp4netnsversion in current nixpkgs-unstable (expected: 1.3.x) - Confirm
bwrap --info-fdflag is available in installed bubblewrap version (added in 0.4.0+) - Confirm
slirp4netns --ready-fdflag is available (added in 0.4.0+) - Test
nix build --no-link --print-out-pathsworks inside the sandbox (nix daemon socket is already mounted) - Confirm
~/.claude/.credentials.jsonis the correct path on this host (checkls ~/.claude/) - Verify bwrap overlay mount order works:
--bind DIRthen--ro-bind FILE_INSIDE_DIRfor auth passthrough
Sources
- https://github.com/rootless-containers/slirp4netns — v1.3.3 release confirmed June 2025,
--ready-fd,--configure,--disable-host-loopbackflags documented (HIGH confidence) - https://code.claude.com/docs/en/authentication —
~/.claude/.credentials.jsonpath on Linux confirmed from official docs (HIGH confidence) - https://manpages.debian.org/unstable/bubblewrap/bwrap.1.en.html —
--unshare-net,--share-net,--info-fdflags documented (HIGH confidence) - https://repology.org/project/slirp4netns/versions — Version tracking across distributions
- Training data: bash-sourced config file pattern,
nix build --no-link,lib.makeBinPath(HIGH confidence for patterns, verify exact flags)
Stack research for: claudebox v2.0 network isolation, profiles, auth passthrough Researched: 2026-04-10