# Parse claudebox flags SKIP_AUDIT=false DRY_RUN=false CHECK_MODE=false SHELL_MODE=false CLAUDE_ARGS=() while (( $# > 0 )); do case "$1" in --yes|-y) SKIP_AUDIT=true ;; --dry-run) DRY_RUN=true ;; --check) CHECK_MODE=true ;; --shell) SHELL_MODE=true ;; --) shift; CLAUDE_ARGS+=("$@"); break ;; *) CLAUDE_ARGS+=("$1") ;; esac shift done export SKIP_AUDIT # consumed by Plan 02 audit display # --check: verify prerequisites and exit (D-10, UX-05) if [[ "$CHECK_MODE" == true ]]; then pass=true green=$'\033[32m' red=$'\033[31m' yellow=$'\033[33m' reset=$'\033[0m' check_cmd() { if command -v "$1" &>/dev/null; then echo "${green}OK${reset} $1" >&2 else echo "${red}FAIL${reset} $1 -- not found" >&2 pass=false fi } echo "claudebox prerequisites:" >&2 echo "" >&2 check_cmd bwrap check_cmd claude check_cmd git check_cmd curl check_cmd nix if [[ -d "$HOME/.claudebox" ]]; then echo "${green}OK${reset} ~/.claudebox exists" >&2 else echo "${red}FAIL${reset} ~/.claudebox -- not found (will be created on first run)" >&2 fi if [[ -v ANTHROPIC_API_KEY ]]; then echo "${green}OK${reset} ANTHROPIC_API_KEY is set" >&2 else echo "${yellow}WARN${reset} ANTHROPIC_API_KEY is not set" >&2 fi echo "" >&2 if [[ "$pass" == true ]]; then echo "${green}All checks passed.${reset}" >&2 exit 0 else echo "${red}Some checks failed.${reset}" >&2 exit 1 fi fi # ANSI formatting (D-03) if [[ -t 2 ]] && [[ "${NO_COLOR:-}" == "" ]]; then BOLD=$'\033[1m' RESET=$'\033[0m' DIM=$'\033[2m' CYAN=$'\033[36m' YELLOW=$'\033[33m' GREEN=$'\033[32m' RED=$'\033[31m' else BOLD="" RESET="" DIM="" CYAN="" YELLOW="" GREEN="" RED="" fi # Mask sensitive values (D-04) mask_value() { local name="$1" value="$2" local upper="${name^^}" if [[ "$upper" == *KEY* || "$upper" == *TOKEN* || "$upper" == *SECRET* || "$upper" == *PASSWORD* || "$upper" == *CREDENTIAL* ]]; then if (( ${#value} > 11 )); then echo "${value:0:7}...${value: -4}" else echo "***" fi else echo "$value" fi } # SANDBOX_PATH is injected by flake.nix via makeBinPath (only runtimeInputs, no host PATH) # Resolve binary paths from runtimeInputs SANDBOX_BASH="$(command -v bash)" CLAUDE_BIN="$(command -v claude)" # Record CWD CWD=$(pwd) # Ensure ~/.claudebox exists mkdir -p "$HOME/.claudebox" # === Sandbox-aware prompting (AWARE-01, AWARE-02) === # Write SANDBOX.md -- fully managed, overwritten every launch (D-02) cat > "$HOME/.claudebox/SANDBOX.md" << 'SANDBOXEOF' # Sandbox Environment You are running inside a bubblewrap (bwrap) sandbox managed by claudebox. Your filesystem is isolated -- only the current working directory and essential system paths are mounted. ## Installing Tools You have two ways to install tools on the fly: **Comma (preferred for quick one-off commands):** `, ripgrep` runs ripgrep without permanent installation. Comma uses nix-index to find the right package automatically. **Nix shell (for persistent access within the session):** `nix shell nixpkgs#python3 -c python3 script.py` runs a command with a package available. To keep it in your PATH for the session: `nix shell nixpkgs#python3` then use `python3` normally. ## Default Restrictions By default, the following are not mounted into the sandbox: - SSH keys (~/.ssh) - GPG and age keys (~/.gnupg, age key files) - Cloud credentials (~/.aws, ~/.config/gcloud) - Tailscale state If your setup has been customized, some of these may be available. ## Git Your git identity (name and email) is pre-configured from the host. The `safe.directory` setting trusts the mounted working directory. For remote operations, prefer HTTPS URLs over SSH since SSH keys are not available by default. SANDBOXEOF # Ensure CLAUDE.md has @SANDBOX.md import (D-03, D-08, AWARE-01) CLAUDEMD="$HOME/.claudebox/CLAUDE.md" if [[ ! -f "$CLAUDEMD" ]]; then printf '%s\n' "@SANDBOX.md" > "$CLAUDEMD" elif [[ "$(head -1 "$CLAUDEMD")" != "@SANDBOX.md" ]]; then tmp=$(mktemp) { printf '%s\n' "@SANDBOX.md"; cat "$CLAUDEMD"; } > "$tmp" mv "$tmp" "$CLAUDEMD" fi # Generate minimal .gitconfig (D-05) GIT_NAME=$(git config --global user.name 2>/dev/null || echo "Claude User") GIT_EMAIL=$(git config --global user.email 2>/dev/null || echo "claude@localhost") GITCONFIG_TMP=$(mktemp) trap 'rm -f "$GITCONFIG_TMP"' EXIT cat > "$GITCONFIG_TMP" <&2 echo "" >&2 # Sandbox-generated (D-01) echo "${BOLD}Sandbox-generated:${RESET}" >&2 for var in "${AUDIT_SANDBOX_KEYS[@]}"; do if [[ "$var" == "PATH" ]]; then echo " ${GREEN}PATH=${RESET}" >&2 IFS=':' read -ra path_entries <<< "${AUDIT_SANDBOX_VALS[PATH]}" for entry in "${path_entries[@]}"; do echo " ${DIM}${entry}${RESET}" >&2 done else echo " ${GREEN}${var}=${RESET}$(mask_value "$var" "${AUDIT_SANDBOX_VALS[$var]}")" >&2 fi done echo "" >&2 # Host allowlisted (D-01) if (( ${#AUDIT_HOST_KEYS[@]} > 0 )); then echo "${BOLD}Host (allowlisted):${RESET}" >&2 for var in "${AUDIT_HOST_KEYS[@]}"; do echo " ${YELLOW}${var}=${RESET}$(mask_value "$var" "${AUDIT_HOST_VALS[$var]}")" >&2 done echo "" >&2 fi # Extra from CLAUDEBOX_EXTRA_ENV (D-01) if (( ${#AUDIT_EXTRA_KEYS[@]} > 0 )); then echo "${BOLD}Extra (CLAUDEBOX_EXTRA_ENV):${RESET}" >&2 for var in "${AUDIT_EXTRA_KEYS[@]}"; do echo " ${YELLOW}${var}=${RESET}$(mask_value "$var" "${AUDIT_EXTRA_VALS[$var]}")" >&2 done echo "" >&2 fi } # Env audit and confirmation (D-05, D-06, D-07, UX-01, UX-02, UX-03) if [[ "$SKIP_AUDIT" != true && "$DRY_RUN" != true ]]; then print_audit # TTY check (D-06) if [[ -t 0 ]]; then echo -n "Proceed? [Y/n] " >&2 read -r response < /dev/tty response="${response,,}" # lowercase if [[ "$response" == "n" || "$response" == "no" ]]; then echo "Aborted." >&2 exit 1 fi else echo "${RED}Error: stdin is not a terminal. Pass --yes or -y to skip confirmation.${RESET}" >&2 exit 1 fi fi # Build sandbox command if [[ "$SHELL_MODE" == true ]]; then SANDBOX_CMD=("$SANDBOX_BASH" "${CLAUDE_ARGS[@]}") else SANDBOX_CMD=("$CLAUDE_BIN" --dangerously-skip-permissions "${CLAUDE_ARGS[@]}") fi # --dry-run: print the bwrap command without executing (D-09, UX-04) if [[ "$DRY_RUN" == true ]]; then { echo "bwrap \\" echo " --clearenv \\" dry_run_i=0 while (( dry_run_i < ${#ENV_ARGS[@]} )); do printf ' %s %s %q \\\n' "${ENV_ARGS[$dry_run_i]}" "${ENV_ARGS[$((dry_run_i+1))]}" "${ENV_ARGS[$((dry_run_i+2))]}" (( dry_run_i += 3 )) done echo " --tmpfs / \\" echo " --proc /proc \\" echo " --dev /dev \\" echo " --tmpfs /tmp \\" echo " --ro-bind /nix/store /nix/store \\" echo " --bind /nix/var/nix /nix/var/nix \\" echo " --ro-bind /etc/resolv.conf /etc/resolv.conf \\" echo " --ro-bind /etc/ssl /etc/ssl \\" echo " --ro-bind /etc/static /etc/static \\" echo " --ro-bind /etc/passwd /etc/passwd \\" echo " --ro-bind /etc/group /etc/group \\" echo " --ro-bind /etc/hosts /etc/hosts \\" echo " --ro-bind /etc/nsswitch.conf /etc/nsswitch.conf \\" echo " --ro-bind /etc/nix /etc/nix \\" printf ' --symlink %q /usr/bin/env \\\n' "$(readlink -f "$(command -v env)")" echo " --tmpfs $HOME \\" echo " --bind $HOME/.claudebox $HOME/.claude \\" printf ' --ro-bind %q %s/.gitconfig \\\n' "$GITCONFIG_TMP" "$HOME" echo " --bind $CWD $CWD \\" echo " --chdir $CWD \\" printf ' -- %s\n' "${SANDBOX_CMD[*]}" } >&2 exit 0 fi # exec bwrap (SAND-04 through SAND-15, UX-06, D-01) exec bwrap \ --clearenv \ "${ENV_ARGS[@]}" \ --tmpfs / \ --proc /proc \ --dev /dev \ --tmpfs /tmp \ --ro-bind /nix/store /nix/store \ --bind /nix/var/nix /nix/var/nix \ --ro-bind /etc/resolv.conf /etc/resolv.conf \ --ro-bind /etc/ssl /etc/ssl \ --ro-bind /etc/static /etc/static \ --ro-bind /etc/passwd /etc/passwd \ --ro-bind /etc/group /etc/group \ --ro-bind /etc/hosts /etc/hosts \ --ro-bind /etc/nsswitch.conf /etc/nsswitch.conf \ --ro-bind /etc/nix /etc/nix \ --symlink "$(readlink -f "$(command -v env)")" /usr/bin/env \ --tmpfs "$HOME" \ --bind "$HOME/.claudebox" "$HOME/.claude" \ --ro-bind "$GITCONFIG_TMP" "$HOME/.gitconfig" \ --bind "$CWD" "$CWD" \ --chdir "$CWD" \ -- "${SANDBOX_CMD[@]}"