#!/usr/bin/env bash # claude.sh — Manage the isolated Claude Code Docker environment # Usage: ./claude.sh [args] set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml" PROJECT="claude-secure" # ─── 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; } # ─── Dependency check ───────────────────────────────────────────────────────── 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 } # ─── Environment loading ────────────────────────────────────────────────────── 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:-}" ]]; then error "ANTHROPIC_API_KEY is not set." error "Copy .env.example → .env and add your key, or export it in your shell." exit 1 fi } # ─── Workspace volume resolution ────────────────────────────────────────────── # Default: named Docker volume (fully isolated). # Override: export WORKSPACE_DIR=/path/to/project before running. workspace_flag() { if [[ -n "${WORKSPACE_DIR:-}" ]]; then local abs abs="$(realpath "${WORKSPACE_DIR}")" if [[ ! -d "$abs" ]]; then error "WORKSPACE_DIR does not exist: $abs" exit 1 fi echo "--volume ${abs}:/workspace:z" else echo "--volume ${PROJECT}-workspace:/workspace" fi } # ─── Compose wrapper ────────────────────────────────────────────────────────── dc() { docker compose -f "$COMPOSE_FILE" -p "$PROJECT" "$@"; } # ─── Commands ───────────────────────────────────────────────────────────────── cmd_start() { check_deps load_env info "Building images..." dc build info "Starting proxy sidecar..." dc up -d proxy info "Waiting for proxy health check..." dc up -d proxy # no-op if already healthy; compose waits via depends_on info "Launching Claude Code..." # shellcheck disable=SC2046 dc run --rm $(workspace_flag) 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 proxy info "Launching Claude Code..." # shellcheck disable=SC2046 dc run --rm $(workspace_flag) claude "$@" } cmd_update() { check_deps info "Rebuilding images (no cache)..." dc build --no-cache info "Update complete. Run './claude.sh start' to launch." } cmd_logs() { check_deps local svc="${1:-proxy}" dc logs -f "$svc" } cmd_status() { check_deps dc ps } cmd_shell() { check_deps load_env warn "Opening debug shell inside Claude container (non-Claude entrypoint)." # shellcheck disable=SC2046 dc run --rm --entrypoint /bin/bash $(workspace_flag) claude } cmd_help() { cat < [args] Commands: start [args] Build images, start proxy, launch Claude Code run [args] Start proxy if needed, launch Claude Code stop Stop and remove all containers update Rebuild images without cache logs [svc] Tail logs (default: proxy) status Show container status shell Open a bash shell in the Claude container (debug) help Show this message Environment variables: ANTHROPIC_API_KEY Required. Set in .env or exported in your shell. WORKSPACE_DIR Optional. Absolute path to mount as /workspace. Defaults to a named Docker volume (fully isolated). Examples: ./claude.sh start WORKSPACE_DIR=\$HOME/myproject ./claude.sh run ./claude.sh logs proxy ./claude.sh shell EOF } # ─── Dispatch ───────────────────────────────────────────────────────────────── case "${1:-help}" in start) shift; cmd_start "$@" ;; stop) cmd_stop ;; run) shift; cmd_run "$@" ;; 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