- 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
335 lines
13 KiB
Markdown
335 lines
13 KiB
Markdown
# 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
|
|
|
|
```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#<package> -c <command> [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`
|