# 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# -c ` via `, ` 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 ```nix 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 ```bash 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 ```bash 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: 1. `--tmpfs /` (empty root) 2. `--ro-bind /nix/store` (packages) 3. `--tmpfs $HOME` (empty home) 4. `--bind $HOME/.claudebox $HOME/.claude` (config into home) 5. `--bind $PWD $PWD` (working directory -- after home tmpfs if CWD is under HOME) **Confidence:** HIGH -- this is well-documented bwrap behavior. ### Environment Handling ```bash 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 ```bash 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: 1. Takes a command name (e.g., `, python3`) 2. Looks up which nixpkgs package provides that command using `nix-index` database 3. Runs `nix shell nixpkgs# -c [args...]` ### Packaging in nixpkgs - `pkgs.comma` -- the comma binary itself - `pkgs.nix-index` -- the indexer that builds/queries the database - Database: comma needs a pre-built index. Two options: 1. **`nix-index-database`** (flake from nix-community) -- pre-built weekly index, no local indexing needed. This is what you want. 2. `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-index` read-only into the sandbox (if using nix-index-database on the host) - Or use the `nix-index-database` flake's `comma-with-db` package 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 ```nix { 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 - `writeShellApplication` still 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: ```bash 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: ```nix 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: 1. `/nix/store` mounted read-only (packages) 2. `/nix/var/nix` mounted -- needed for `nix` commands to find the database 3. Nix daemon socket: `/nix/var/nix/daemon-socket/socket` -- needed for `nix shell` to trigger builds/downloads 4. `NIX_PATH` or registry config for `nixpkgs` resolution The daemon socket is critical and easy to miss. Without it, `nix shell` fails because it can't talk to the Nix daemon. ```bash --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: ```bash --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: 1. **Build test:** `nix build` succeeds (shellcheck passes) 2. **Smoke test:** `claudebox` launches, `env` inside shows only allowlisted vars 3. **Secret test:** Verify `ls ~/.ssh` fails, `cat /etc/shadow` fails, `env | grep -i key` returns nothing 4. **Comma test:** `, python3 --version` works inside sandbox 5. **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 `bubblewrap` version in current nixpkgs channel - [ ] Confirm `comma` and `nix-index-database` flake are current and compatible - [ ] Test Nix daemon socket access through bwrap bind mount - [ ] Test mount ordering with CWD under $HOME - [ ] Confirm `--clearenv` + `--setenv` pattern works with Claude Code (it may need vars we haven't listed) - [ ] Check if Claude Code needs `~/.local` or `~/.config` beyond `~/.claude`