401 lines
12 KiB
Bash
401 lines
12 KiB
Bash
# 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"
|
|
|
|
# Credential file mount (AUTH-01, AUTH-02)
|
|
# Use ~/.claudebox (the host-side claudebox config dir), not ~/.claude
|
|
# ~/.claude -> ~/.claudebox symlink only exists inside the sandbox at runtime
|
|
CREDS_FILE="$HOME/.claudebox/.credentials.json"
|
|
if [[ -f "$CREDS_FILE" ]]; then
|
|
CREDS_MOUNT=true
|
|
else
|
|
CREDS_MOUNT=false
|
|
fi
|
|
|
|
# Claude Code config file mount (~/.claude.json)
|
|
# Stores auth tokens and user preferences; must be read-write so Claude Code
|
|
# can update tokens and write backups without prompting for re-auth.
|
|
CLAUDE_JSON_FILE="$HOME/.claude.json"
|
|
if [[ -f "$CLAUDE_JSON_FILE" ]]; then
|
|
CLAUDE_JSON_MOUNT=true
|
|
else
|
|
CLAUDE_JSON_MOUNT=false
|
|
fi
|
|
|
|
# === 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. Both ~/.claude and ~/.claudebox
|
|
point to the same directory inside the sandbox.
|
|
|
|
## 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" <<GITEOF
|
|
[user]
|
|
name = $GIT_NAME
|
|
email = $GIT_EMAIL
|
|
[safe]
|
|
directory = *
|
|
GITEOF
|
|
|
|
# Parallel display data for env audit (D-01)
|
|
declare -a AUDIT_SANDBOX_KEYS=()
|
|
declare -A AUDIT_SANDBOX_VALS=()
|
|
declare -a AUDIT_HOST_KEYS=()
|
|
declare -A AUDIT_HOST_VALS=()
|
|
declare -a AUDIT_EXTRA_KEYS=()
|
|
declare -A AUDIT_EXTRA_VALS=()
|
|
|
|
# Build environment --setenv args array (D-03, D-04, SAND-02, SAND-03)
|
|
# Sandbox-generated vars -- set directly, never from host
|
|
ENV_ARGS=(
|
|
--setenv HOME "$HOME"
|
|
--setenv USER "$USER"
|
|
--setenv PATH "$SANDBOX_PATH"
|
|
--setenv SHELL "$SANDBOX_BASH"
|
|
--setenv TMPDIR /tmp
|
|
--setenv XDG_RUNTIME_DIR /tmp
|
|
--setenv NIX_SSL_CERT_FILE /etc/ssl/certs/ca-certificates.crt
|
|
--setenv SSL_CERT_FILE /etc/ssl/certs/ca-certificates.crt
|
|
)
|
|
|
|
# Populate sandbox audit data
|
|
AUDIT_SANDBOX_KEYS=(HOME USER PATH SHELL TMPDIR XDG_RUNTIME_DIR NIX_SSL_CERT_FILE SSL_CERT_FILE)
|
|
AUDIT_SANDBOX_VALS[HOME]="$HOME"
|
|
AUDIT_SANDBOX_VALS[USER]="$USER"
|
|
AUDIT_SANDBOX_VALS[PATH]="$SANDBOX_PATH"
|
|
AUDIT_SANDBOX_VALS[SHELL]="$SANDBOX_BASH"
|
|
AUDIT_SANDBOX_VALS[TMPDIR]="/tmp"
|
|
AUDIT_SANDBOX_VALS[XDG_RUNTIME_DIR]="/tmp"
|
|
AUDIT_SANDBOX_VALS[NIX_SSL_CERT_FILE]="/etc/ssl/certs/ca-certificates.crt"
|
|
AUDIT_SANDBOX_VALS[SSL_CERT_FILE]="/etc/ssl/certs/ca-certificates.crt"
|
|
|
|
# Allowlisted host vars -- only pass if set on host
|
|
HOST_ALLOWLIST=(TERM EDITOR LANG LC_ALL ANTHROPIC_API_KEY)
|
|
for var in "${HOST_ALLOWLIST[@]}"; do
|
|
if [[ -v "$var" ]]; then
|
|
ENV_ARGS+=(--setenv "$var" "${!var}")
|
|
AUDIT_HOST_KEYS+=("$var")
|
|
AUDIT_HOST_VALS[$var]="${!var}"
|
|
fi
|
|
done
|
|
|
|
# CLAUDEBOX_EXTRA_ENV escape hatch (D-03, comma-separated)
|
|
if [[ -v CLAUDEBOX_EXTRA_ENV ]]; then
|
|
IFS=',' read -ra EXTRAS <<< "$CLAUDEBOX_EXTRA_ENV"
|
|
for var in "${EXTRAS[@]}"; do
|
|
var="${var// /}" # trim whitespace
|
|
if [[ -n "$var" ]] && [[ -v "$var" ]]; then
|
|
ENV_ARGS+=(--setenv "$var" "${!var}")
|
|
AUDIT_EXTRA_KEYS+=("$var")
|
|
AUDIT_EXTRA_VALS[$var]="${!var}"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Env audit display (D-01, D-02, D-03, D-04, D-07, UX-01)
|
|
print_audit() {
|
|
echo "${BOLD}${CYAN}=== Sandbox Environment ===${RESET}" >&2
|
|
echo "" >&2
|
|
|
|
# Unified env list: sandbox [~], host allowlisted [>], extra [+] (D-06, D-07, D-08, D-09, D-10)
|
|
for var in "${AUDIT_SANDBOX_KEYS[@]}"; do
|
|
if [[ "$var" == "PATH" ]]; then
|
|
echo " ${GREEN}[~]${RESET} PATH=" >&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}[~]${RESET} ${var}=$(mask_value "$var" "${AUDIT_SANDBOX_VALS[$var]}")" >&2
|
|
fi
|
|
done
|
|
|
|
for var in "${AUDIT_HOST_KEYS[@]}"; do
|
|
echo " ${YELLOW}[>]${RESET} ${var}=$(mask_value "$var" "${AUDIT_HOST_VALS[$var]}")" >&2
|
|
done
|
|
|
|
for var in "${AUDIT_EXTRA_KEYS[@]}"; do
|
|
echo " ${CYAN}[+]${RESET} ${var}=$(mask_value "$var" "${AUDIT_EXTRA_VALS[$var]}")" >&2
|
|
done
|
|
|
|
echo "" >&2
|
|
|
|
# Mounts section
|
|
echo "${BOLD}Mounts:${RESET}" >&2
|
|
printf ' %-12s %s (read-write)\n' "CWD" "$CWD" >&2
|
|
printf ' %-12s %s (read-write)\n' "$HOME/.claude" "$HOME/.claudebox" >&2
|
|
if [[ "$CLAUDE_JSON_MOUNT" == true ]]; then
|
|
printf ' %-12s %s (read-write)\n' "$HOME/.claude.json" "$CLAUDE_JSON_FILE" >&2
|
|
fi
|
|
if [[ "$CREDS_MOUNT" == true ]]; then
|
|
printf ' %-12s %s (read-write)\n' "credentials" "$CREDS_FILE" >&2
|
|
fi
|
|
|
|
echo "" >&2
|
|
|
|
# Network section (Phase 4 placeholder — full isolation comes in Phase 6)
|
|
echo "${BOLD}Network:${RESET}" >&2
|
|
echo " full (host network)" >&2
|
|
}
|
|
|
|
# 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 \\"
|
|
# Guard: ENV_ARGS must be a multiple of 3 (--setenv NAME VALUE triplets)
|
|
if (( ${#ENV_ARGS[@]} % 3 != 0 )); then
|
|
echo "BUG: ENV_ARGS length ${#ENV_ARGS[@]} is not a multiple of 3" >&2
|
|
exit 1
|
|
fi
|
|
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=$(( 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/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/.claudebox \\"
|
|
echo " --symlink $HOME/.claudebox $HOME/.claude \\"
|
|
if [[ "$CLAUDE_JSON_MOUNT" == true ]]; then
|
|
echo " --bind $CLAUDE_JSON_FILE $HOME/.claude.json \\"
|
|
fi
|
|
if [[ "$CREDS_MOUNT" == true ]]; then
|
|
echo " --bind $CREDS_FILE $HOME/.claudebox/.credentials.json \\"
|
|
fi
|
|
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
|
|
|
|
# Build bwrap mount args array (allows conditional mounts)
|
|
BWRAP_ARGS=(
|
|
--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/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/.claudebox"
|
|
--symlink "$HOME/.claudebox" "$HOME/.claude"
|
|
)
|
|
if [[ "$CLAUDE_JSON_MOUNT" == true ]]; then
|
|
BWRAP_ARGS+=(--bind "$CLAUDE_JSON_FILE" "$HOME/.claude.json")
|
|
fi
|
|
if [[ "$CREDS_MOUNT" == true ]]; then
|
|
BWRAP_ARGS+=(--bind "$CREDS_FILE" "$HOME/.claudebox/.credentials.json")
|
|
fi
|
|
BWRAP_ARGS+=(
|
|
--ro-bind "$GITCONFIG_TMP" "$HOME/.gitconfig"
|
|
--bind "$CWD" "$CWD"
|
|
--chdir "$CWD"
|
|
--
|
|
"${SANDBOX_CMD[@]}"
|
|
)
|
|
|
|
# exec bwrap (SAND-04 through SAND-15, UX-06, D-01)
|
|
exec bwrap "${BWRAP_ARGS[@]}"
|