- Add CREDS_FILE/CREDS_MOUNT detection after mkdir ~/.claudebox - Conditional --bind in exec bwrap via BWRAP_ARGS array - Mirror conditional bind in --dry-run display block - Read-write mount (not ro-bind) for OAuth token refresh - Silent skip when credentials file absent (no error/warning) - Refactor exec bwrap to BWRAP_ARGS array for conditional mount support
13 KiB
Technology Stack
Project: claudebox Researched: 2026-04-09 Note: Research conducted from training data only (web/shell tools unavailable). Versions should be verified against current nixpkgs before implementation.
Recommended Stack
Core: Nix Derivation via writeShellApplication
| Technology | Version | Purpose | Why | Confidence |
|---|---|---|---|---|
writeShellApplication |
nixpkgs stable | Produce the claudebox wrapper script |
Generates a shellcheck-validated bash script in the Nix store with runtime PATH wired to declared runtimeInputs. Superior to writeShellScriptBin because it runs shellcheck at build time and sets set -euo pipefail automatically. |
HIGH |
bubblewrap (bwrap) |
0.9.x+ | Sandbox runtime | Unprivileged user-namespace sandbox. In nixpkgs as bubblewrap. No setuid needed on NixOS (user namespaces enabled by default). |
HIGH |
claude-code |
CLI | The wrapped tool | Provided by Anthropic's npm/standalone installer. Assumed pre-installed or passed as input. | HIGH |
Runtime Dependencies (runtimeInputs)
| Package | Purpose | Why This One | Confidence |
|---|---|---|---|
bubblewrap |
Sandbox | The whole point | HIGH |
coreutils |
Basic shell utils | env, cat, echo, mkdir, etc. |
HIGH |
git |
Version control | Claude Code requires git for repo operations | HIGH |
curl |
HTTP requests | Claude Code's MCP and tool use | HIGH |
jq |
JSON processing | Env audit display, config manipulation | HIGH |
ripgrep |
Search | Claude Code's preferred grep | HIGH |
fd |
File finding | Claude Code's preferred find | HIGH |
nix |
Package manager | Required for nix shell inside sandbox |
HIGH |
comma |
On-demand packages | Runs nix shell nixpkgs#<pkg> -c <cmd> via , <cmd> syntax |
HIGH |
nix-index |
Package database | Required by comma to resolve command -> package mapping | HIGH |
bash |
Shell | bwrap needs a shell to exec into | HIGH |
nodejs |
Runtime | Claude Code is a Node.js application | HIGH |
NOT in runtimeInputs (Important)
| Package | Why Excluded |
|---|---|
gnupg |
Secret material -- explicitly hidden |
openssh |
Secret material -- explicitly hidden |
age / agenix |
Secret material -- explicitly hidden |
tailscale |
Infrastructure access -- explicitly hidden |
Key Nix Functions
writeShellApplication -- Use This
pkgs.writeShellApplication {
name = "claudebox";
runtimeInputs = with pkgs; [
bubblewrap coreutils git curl jq ripgrep fd
nix comma nix-index bash nodejs
];
text = ''
# Script body here -- bwrap invocation
# PATH is automatically set to include all runtimeInputs
# set -euo pipefail is automatic
# shellcheck runs at build time
'';
}
Confidence: HIGH -- this is the standard nixpkgs pattern for wrapper scripts.
Why NOT These Alternatives
| Alternative | Why Not |
|---|---|
writeShellScriptBin |
No shellcheck, no automatic set -euo pipefail, no runtimeInputs wiring. You'd have to manually construct PATH. |
makeWrapper / wrapProgram |
Designed for wrapping existing binaries with env vars/flags. Overkill and wrong abstraction -- we're writing a new script, not patching an existing binary. |
symlinkJoin + makeWrapper |
Pattern for combining multiple derivations. Not needed -- we have one script. |
stdenv.mkDerivation |
Too heavy. writeShellApplication is a specialized shortcut for exactly this use case. |
runCommand / writeScript |
Lower-level, no shellcheck, no runtimeInputs. |
Bubblewrap Flags
Namespace Isolation
bwrap \
--unshare-user # New user namespace (maps to root inside)
--unshare-pid # New PID namespace (can't see host PIDs)
--unshare-ipc # New IPC namespace
--unshare-uts # New UTS namespace (hostname isolation)
# NOTE: --unshare-net is OUT OF SCOPE per project constraints
# NOTE: --unshare-cgroup usually unnecessary for this use case
Confidence: HIGH -- these are standard bwrap namespace flags.
Filesystem Mounts
bwrap \
# Empty root
--tmpfs / # Start with empty tmpfs root
# Nix store (read-only, required for nix/comma)
--ro-bind /nix/store /nix/store # All Nix packages
--ro-bind /nix/var /nix/var # Nix DB (needed for nix commands)
# System essentials
--ro-bind /etc/resolv.conf /etc/resolv.conf # DNS resolution
--ro-bind /etc/ssl /etc/ssl # TLS certificates
--ro-bind /etc/nix /etc/nix # Nix config (substituters, etc.)
--proc /proc # /proc filesystem
--dev /dev # Device nodes
# Working directory (read-write)
--bind "$PWD" "$PWD" # CWD mounted read-write
# Claude config (remapped)
--bind "$HOME/.claudebox" "$HOME/.claude" # Persistent config
# Home directory structure
--tmpfs "$HOME" # Empty home
--bind "$PWD" "$PWD" # Re-bind CWD after home tmpfs
# Temp
--tmpfs /tmp # Writable tmp
--tmpfs /run # Writable run
Confidence: HIGH for the pattern, MEDIUM for exact mount ordering (test on NixOS to confirm).
Mount Ordering Matters
bwrap processes mounts in order. Later mounts can overlay earlier ones. The correct order is:
--tmpfs /(empty root)--ro-bind /nix/store(packages)--tmpfs $HOME(empty home)--bind $HOME/.claudebox $HOME/.claude(config into home)--bind $PWD $PWD(working directory -- after home tmpfs if CWD is under HOME)
Confidence: HIGH -- this is well-documented bwrap behavior.
Environment Handling
bwrap \
--clearenv # Start with empty environment
--setenv HOME "$HOME" # Explicit home
--setenv PATH "$SANDBOX_PATH" # Controlled PATH
--setenv TERM "$TERM" # Terminal type
--setenv LANG "$LANG" # Locale
--setenv EDITOR "nano" # Safe editor (no vim with plugin configs)
--setenv USER "$USER" # Username
--setenv SHELL "/bin/bash" # Shell
--setenv NIX_PATH "$NIX_PATH" # For nix-env/comma
--setenv XDG_CACHE_HOME "$HOME/.cache"
--setenv TMPDIR /tmp
--clearenv is the critical flag. This implements the allowlist model: start empty, add explicitly. Without it, you're doing denylist (trying to --unsetenv secrets you know about -- guaranteed to miss some).
Confidence: HIGH
Process Execution
bwrap \
--die-with-parent # Kill sandbox if parent dies
--new-session # New session (prevents tty hijacking)
-- \
claude --dangerously-skip-permissions "$@"
Confidence: HIGH
Comma and nix-index
How Comma Works
comma (the , command) is a wrapper that:
- Takes a command name (e.g.,
, python3) - Looks up which nixpkgs package provides that command using
nix-indexdatabase - Runs
nix shell nixpkgs#<package> -c <command> [args...]
Packaging in nixpkgs
pkgs.comma-- the comma binary itselfpkgs.nix-index-- the indexer that builds/queries the database- Database: comma needs a pre-built index. Two options:
nix-index-database(flake from nix-community) -- pre-built weekly index, no local indexing needed. This is what you want.nix-index --update-- builds index locally, takes 30+ minutes. Don't do this.
For claudebox
The nix-index database needs to be available inside the sandbox. Options:
- Mount
~/.cache/nix-indexread-only into the sandbox (if using nix-index-database on the host) - Or use the
nix-index-databaseflake'scomma-with-dbpackage which bundles the database
Recommendation: Use comma-with-db from nix-index-database flake if available, otherwise mount the host's nix-index database read-only.
Confidence: MEDIUM -- comma-with-db packaging may have changed. Verify against current nix-community/nix-index-database flake.
Flake Structure
{
description = "claudebox - sandboxed Claude Code wrapper";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
# Optional: pre-built nix-index database
nix-index-database = {
url = "github:nix-community/nix-index-database";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, nix-index-database, ... }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in {
packages.${system}.default = pkgs.writeShellApplication {
name = "claudebox";
runtimeInputs = with pkgs; [
bubblewrap coreutils git curl jq ripgrep fd
nix bash nodejs
# comma with bundled database from nix-index-database flake
];
text = builtins.readFile ./claudebox.sh;
};
# Or as an overlay for integration with host NixOS config
overlays.default = final: prev: {
claudebox = self.packages.${final.system}.default;
};
};
}
Confidence: HIGH for the pattern, MEDIUM for nix-index-database integration specifics.
Why builtins.readFile for the Script Body
Keep the shell script in a separate claudebox.sh file rather than inline in Nix. Reasons:
- Shell syntax highlighting in editors
- Shellcheck can run independently
- Easier to iterate on the script without touching Nix expressions
writeShellApplicationstill runs shellcheck on it at build time
Confidence: HIGH
PATH Construction Inside Sandbox
The sandbox PATH should only contain Nix store paths. writeShellApplication handles this for the wrapper script itself, but the PATH inside the bwrap sandbox needs to be constructed explicitly:
SANDBOX_PATH=""
for pkg in ${coreutils} ${git} ${curl} ${jq} ${ripgrep} ${fd} ${nix} ${comma} ${bash} ${nodejs}; do
SANDBOX_PATH="$SANDBOX_PATH:$pkg/bin"
done
SANDBOX_PATH="${SANDBOX_PATH#:}" # Remove leading colon
Or more idiomatically in Nix, construct the PATH in the Nix expression and interpolate it into the script:
let
sandboxPath = lib.makeBinPath [
pkgs.coreutils pkgs.git pkgs.curl pkgs.jq
pkgs.ripgrep pkgs.fd pkgs.nix pkgs.comma
pkgs.bash pkgs.nodejs
];
in
pkgs.writeShellApplication {
text = ''
SANDBOX_PATH="${sandboxPath}"
# ... bwrap --setenv PATH "$SANDBOX_PATH" ...
'';
}
Use lib.makeBinPath -- it joins /nix/store/.../bin paths with colons. This is the standard nixpkgs function for constructing PATH.
Confidence: HIGH
Nix Store Access Inside Sandbox
For nix shell and comma to work inside the sandbox, you need:
/nix/storemounted read-only (packages)/nix/var/nixmounted -- needed fornixcommands to find the database- Nix daemon socket:
/nix/var/nix/daemon-socket/socket-- needed fornix shellto trigger builds/downloads NIX_PATHor registry config fornixpkgsresolution
The daemon socket is critical and easy to miss. Without it, nix shell fails because it can't talk to the Nix daemon.
--ro-bind /nix/store /nix/store
--ro-bind /nix/var/nix/db /nix/var/nix/db
--ro-bind /nix/var/nix/daemon-socket /nix/var/nix/daemon-socket
Wait -- the daemon socket needs to be bind-mounted (not ro-bind) because it's a Unix socket:
--bind /nix/var/nix/daemon-socket /nix/var/nix/daemon-socket
Confidence: MEDIUM -- socket bind-mount behavior should be tested. The daemon socket may need --bind not --ro-bind.
Testing Strategy
Since this is a shell script wrapped in Nix:
- Build test:
nix buildsucceeds (shellcheck passes) - Smoke test:
claudeboxlaunches,envinside shows only allowlisted vars - Secret test: Verify
ls ~/.sshfails,cat /etc/shadowfails,env | grep -i keyreturns nothing - Comma test:
, python3 --versionworks inside sandbox - Mount test: Can write to CWD, cannot write outside CWD
No test framework needed. A simple test.sh with assertions suffices.
Confidence: HIGH
Sources
- Training data knowledge of nixpkgs
writeShellApplication(stable API since 2022) - Training data knowledge of bubblewrap (stable API, project is mature)
- Training data knowledge of comma/nix-index-database (nix-community project)
- All versions should be verified against current nixpkgs before implementation
Verification Checklist (for implementation phase)
- Confirm
bubblewrapversion in current nixpkgs channel - Confirm
commaandnix-index-databaseflake are current and compatible - Test Nix daemon socket access through bwrap bind mount
- Test mount ordering with CWD under $HOME
- Confirm
--clearenv+--setenvpattern works with Claude Code (it may need vars we haven't listed) - Check if Claude Code needs
~/.localor~/.configbeyond~/.claude