#!/usr/bin/env bash # claude.sh — Manage the isolated Claude Code Docker environment # 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" ALLOW_KUBE=0 # ─── Colours ────────────────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' info() { echo -e "${GREEN}[+]${NC} $*"; } warn() { echo -e "${YELLOW}[!]${NC} $*"; } error() { echo -e "${RED}[-]${NC} $*" >&2; } # ─── Helpers ────────────────────────────────────────────────────────────────── check_deps() { 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; } } load_env() { local env_file="$SCRIPT_DIR/.env" if [[ -f "$env_file" ]]; then # shellcheck disable=SC1090 set -a; source "$env_file"; set +a fi if [[ -z "${ANTHROPIC_API_KEY:-}" && -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then warn "No ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN found." 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): log in when prompted; port 54545 must be reachable from your browser." fi } # Wrapper so every docker compose call uses the right file and project name. 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 local -a exact_blocked=( / "$HOME" /root /home ) for dir in "${exact_blocked[@]}"; do [[ "$cwd" == "$dir" ]] && { error "Refusing to mount $cwd as workspace — too broad. cd into a project subdirectory first." exit 1 } done # 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 } # 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 [[ "$cwd" == "$dir" || "$cwd" == "$dir/"* ]] && { error "Refusing to mount $cwd as workspace — contains sensitive data. cd into a project subdirectory first." exit 1 } done VOLUME_ARGS=("--volume" "${cwd}:/workspace:z") 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 } # ─── Commands ───────────────────────────────────────────────────────────────── cmd_start() { check_deps; load_env; build_volume_args info "Pulling latest images..." dc pull info "Starting proxy sidecar..." dc up -d proxy info "Launching Claude Code..." dc run --rm --service-ports "${VOLUME_ARGS[@]}" claude "$@" } cmd_stop() { check_deps info "Stopping all containers..." dc down } cmd_update() { check_deps info "Pulling latest images from registry..." dc pull info "Update complete. Run './claude.sh start' to launch." } cmd_logs() { check_deps dc logs -f "${1:-proxy}" } cmd_status() { check_deps dc ps } cmd_shell() { check_deps; load_env; build_volume_args warn "Opening debug shell inside Claude container (non-Claude entrypoint)." dc run --rm --service-ports --entrypoint /bin/bash "${VOLUME_ARGS[@]}" claude } cmd_help() { cat < [args] Commands: start [args] Start proxy, launch Claude Code (CLI) stop Stop and remove all containers update Pull latest images from the registry logs [svc] Tail logs (default: proxy) status Show container status shell Open a bash shell in the Claude container (debug) help Show this message 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) Examples: cd ~/myproject && ./claude.sh start cd ~/myproject && ./claude.sh --kube start ./claude.sh logs proxy ./claude.sh shell EOF } # ─── Dispatch ───────────────────────────────────────────────────────────────── while [[ "${1:-}" == --* ]]; do case "$1" in --kube) ALLOW_KUBE=1; shift ;; *) break ;; esac done case "${1:-help}" in start|run) shift; cmd_start "$@" ;; stop) cmd_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 exit 1 ;; esac