diff --git a/claude.sh b/claude.sh old mode 100644 new mode 100755 index 4e38739..0ca20a8 --- a/claude.sh +++ b/claude.sh @@ -1,14 +1,12 @@ #!/usr/bin/env bash # claude.sh — Manage the isolated Claude Code Docker environment -# Usage: ./claude.sh [args] +# Usage: ./claude.sh [--kube] [args] set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml" PROJECT="claude-secure" - -# ─── Global flags ───────────────────────────────────────────────────────────── -ALLOW_KUBE=0 # set by --kube before the subcommand +ALLOW_KUBE=0 # ─── Colours ────────────────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' @@ -16,19 +14,14 @@ info() { echo -e "${GREEN}[+]${NC} $*"; } warn() { echo -e "${YELLOW}[!]${NC} $*"; } error() { echo -e "${RED}[-]${NC} $*" >&2; } -# ─── Dependency check ───────────────────────────────────────────────────────── +# ─── Helpers ────────────────────────────────────────────────────────────────── check_deps() { - if ! command -v docker &>/dev/null; then - error "Docker is not installed. https://docs.docker.com/get-docker/" - exit 1 - fi - if ! docker compose version &>/dev/null 2>&1; then - error "Docker Compose v2 plugin is required." - exit 1 - fi + command -v docker &>/dev/null \ + || { error "Docker is not installed. https://docs.docker.com/get-docker/"; exit 1; } + docker compose version &>/dev/null 2>&1 \ + || { error "Docker Compose v2 plugin is required."; exit 1; } } -# ─── Environment loading ────────────────────────────────────────────────────── load_env() { local env_file="$SCRIPT_DIR/.env" if [[ -f "$env_file" ]]; then @@ -40,110 +33,67 @@ load_env() { warn "Claude Code will prompt you to authenticate on first run." warn " Option 1 (API key): set ANTHROPIC_API_KEY in .env" warn " Option 2 (token): run 'claude setup-token' and set CLAUDE_CODE_OAUTH_TOKEN in .env" - warn " Option 3 (browser): run './claude.sh run' and follow the login prompt;" - warn " port 54545 must be reachable from your browser." + warn " Option 3 (browser): log in when prompted; port 54545 must be reachable from your browser." fi } -# ─── Workspace volume resolution ────────────────────────────────────────────── -# Mounts the current working directory as /workspace inside the container. -# Refuses to mount home directories, key material, or system directories. -workspace_flag() { +dc() { docker compose -f "$COMPOSE_FILE" -p "$PROJECT" "$@"; } + +# ─── Volume args ────────────────────────────────────────────────────────────── +# Builds VOLUME_ARGS array for docker compose run. +# Validates the workspace path and optionally adds the kubeconfig mount. +build_volume_args() { local cwd cwd="$(pwd)" - # Exact-match blocklist — mounting these exposes too much of the host - local -a exact_blocked=( - / - "$HOME" - /root - /home - ) - - # Prefix blocklist — block these paths and all subdirectories. - # Covers system internals and credential/key material. - local -a prefix_blocked=( - /bin /sbin /lib /lib64 - /etc /usr /var - /proc /sys /dev - /boot /run - # SSH keys - "$HOME/.ssh" - /root/.ssh - # PGP/GPG keys - "$HOME/.gnupg" - /root/.gnupg - ) - + # Exact-match blocklist + local -a exact_blocked=( / "$HOME" /root /home ) for dir in "${exact_blocked[@]}"; do - if [[ "$cwd" == "$dir" ]]; then - error "Refusing to mount $cwd as workspace — too broad." - error "cd into a project subdirectory first." + [[ "$cwd" == "$dir" ]] && { + error "Refusing to mount $cwd as workspace — too broad. cd into a project subdirectory first." exit 1 - fi + } done - # Block any user home directory directly under /home (e.g. /home/alice) - if [[ "$cwd" =~ ^/home/[^/]+$ ]]; then - error "Refusing to mount $cwd as workspace — user home directory." - error "cd into a project subdirectory first." + # Any user home directory directly under /home + [[ "$cwd" =~ ^/home/[^/]+$ ]] && { + error "Refusing to mount $cwd as workspace — user home directory. cd into a project subdirectory first." exit 1 - fi + } + # Prefix blocklist — system internals and credential/key material + local -a prefix_blocked=( + /bin /sbin /lib /lib64 /etc /usr /var /proc /sys /dev /boot /run + "$HOME/.ssh" /root/.ssh "$HOME/.gnupg" /root/.gnupg + ) for dir in "${prefix_blocked[@]}"; do - if [[ "$cwd" == "$dir" || "$cwd" == "$dir/"* ]]; then - error "Refusing to mount $cwd as workspace — contains sensitive data." - error "cd into a project subdirectory first." + [[ "$cwd" == "$dir" || "$cwd" == "$dir/"* ]] && { + error "Refusing to mount $cwd as workspace — contains sensitive data. cd into a project subdirectory first." exit 1 - fi + } done - echo "--volume ${cwd}:/workspace:z" -} + VOLUME_ARGS=("--volume" "${cwd}:/workspace:z") -# ─── Optional kubeconfig mount ──────────────────────────────────────────────── -# Enabled by passing --kube before the subcommand. -# Mounts $HOME/.kube read-only at /home/node/.kube inside the container. -kube_flag() { - [[ "$ALLOW_KUBE" -eq 0 ]] && return - local kube_dir="$HOME/.kube" - if [[ ! -d "$kube_dir" ]]; then - error "--kube specified but $kube_dir does not exist." - exit 1 + if [[ "$ALLOW_KUBE" -eq 1 ]]; then + [[ -d "$HOME/.kube" ]] || { error "--kube: $HOME/.kube does not exist."; exit 1; } + VOLUME_ARGS+=("--volume" "$HOME/.kube:/home/node/.kube:ro,z") fi - echo "--volume ${kube_dir}:/home/node/.kube:ro,z" } -# ─── Compose wrapper ────────────────────────────────────────────────────────── -dc() { docker compose -f "$COMPOSE_FILE" -p "$PROJECT" "$@"; } - # ─── Commands ───────────────────────────────────────────────────────────────── - cmd_start() { - check_deps - load_env + check_deps; load_env; build_volume_args info "Starting proxy sidecar..." dc up -d --no-build proxy info "Launching Claude Code..." - # shellcheck disable=SC2046 - dc run --rm --no-build --service-ports $(workspace_flag) $(kube_flag) claude "$@" + dc run --rm --no-build --service-ports "${VOLUME_ARGS[@]}" claude "$@" } cmd_stop() { check_deps info "Stopping all containers..." dc down - info "Done." -} - -cmd_run() { - check_deps - load_env - info "Ensuring proxy is running..." - dc up -d --no-build proxy - info "Launching Claude Code..." - # shellcheck disable=SC2046 - dc run --rm --no-build --service-ports $(workspace_flag) $(kube_flag) claude "$@" } cmd_update() { @@ -155,8 +105,7 @@ cmd_update() { cmd_logs() { check_deps - local svc="${1:-proxy}" - dc logs -f "$svc" + dc logs -f "${1:-proxy}" } cmd_status() { @@ -165,44 +114,34 @@ cmd_status() { } cmd_shell() { - check_deps - load_env + check_deps; load_env; build_volume_args warn "Opening debug shell inside Claude container (non-Claude entrypoint)." - # shellcheck disable=SC2046 - dc run --rm --no-build --service-ports --entrypoint /bin/bash $(workspace_flag) $(kube_flag) claude + dc run --rm --no-build --service-ports --entrypoint /bin/bash "${VOLUME_ARGS[@]}" claude } cmd_web() { - check_deps - load_env - if [[ -z "${WEBUI_PASSWORD:-}" ]]; then - error "WEBUI_PASSWORD is not set. Add it to .env before starting the web interface." - exit 1 - fi + check_deps; load_env + [[ -n "${WEBUI_PASSWORD:-}" ]] \ + || { error "WEBUI_PASSWORD is not set. Add it to .env before starting the web interface."; exit 1; } info "Starting proxy and web interface..." dc up -d --no-build webui - local port=7681 - info "Web interface is up → http://0.0.0.0:${port}" + info "Web interface is up → http://0.0.0.0:7681" info "Credentials: ${WEBUI_USER:-claude} / [WEBUI_PASSWORD]" - warn "To reach it from outside this host, publish the port:" - warn " sbx ports --publish ${port}:${port}/tcp" } cmd_web_stop() { check_deps info "Stopping web interface..." - dc stop webui - dc rm -f webui + dc stop webui && dc rm -f webui } cmd_help() { cat < [args] +Usage: $(basename "$0") [--kube] [args] Commands: start [args] Start proxy, launch Claude Code (CLI) - run [args] Start proxy if needed, launch Claude Code (CLI) - web Start proxy + web interface (browser terminal) + web Start proxy + web interface (browser terminal on :7681) web-stop Stop the web interface (keeps proxy running) stop Stop and remove all containers update Pull latest images from the registry @@ -211,26 +150,26 @@ Commands: shell Open a bash shell in the Claude container (debug) help Show this message -Environment variables (set in .env or shell): - ANTHROPIC_API_KEY Required for all modes. - WEBUI_USER Web interface username (default: claude). - WEBUI_PASSWORD Required for web mode. Basic auth password. - Flags (before the subcommand): --kube Mount \$HOME/.kube read-only at /home/node/.kube (kubectl access) +Environment variables (set in .env): + ANTHROPIC_API_KEY API key auth + CLAUDE_CODE_OAUTH_TOKEN OAuth token auth (from 'claude setup-token') + IMAGE_TAG Image tag to use (default: latest) + WEBUI_USER Web interface username (default: claude) + WEBUI_PASSWORD Required for web mode + Examples: cd ~/myproject && ./claude.sh start cd ~/myproject && ./claude.sh --kube start ./claude.sh web ./claude.sh logs proxy - ./claude.sh logs webui ./claude.sh shell EOF } # ─── Dispatch ───────────────────────────────────────────────────────────────── -# Parse global flags before the subcommand while [[ "${1:-}" == --* ]]; do case "$1" in --kube) ALLOW_KUBE=1; shift ;; @@ -239,16 +178,15 @@ while [[ "${1:-}" == --* ]]; do done case "${1:-help}" in - start) shift; cmd_start "$@" ;; - stop) cmd_stop ;; - run) shift; cmd_run "$@" ;; - web) cmd_web ;; - web-stop) cmd_web_stop ;; - update) cmd_update ;; - logs) shift; cmd_logs "${1:-}" ;; - status) cmd_status ;; - shell) cmd_shell ;; - help|-h|--help) cmd_help ;; + start|run) shift; cmd_start "$@" ;; + stop) cmd_stop ;; + web) cmd_web ;; + web-stop) cmd_web_stop ;; + update) cmd_update ;; + logs) shift; cmd_logs "${1:-}" ;; + status) cmd_status ;; + shell) cmd_shell ;; + help|-h|--help) cmd_help ;; *) error "Unknown command: ${1}" cmd_help