docker-claude/claude.sh

264 lines
9.1 KiB
Bash
Raw Normal View History

2026-04-14 20:11:24 +02:00
#!/usr/bin/env bash
# claude.sh — Manage the isolated Claude Code Docker environment
# Usage: ./claude.sh <command> [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
2026-04-14 20:11:24 +02:00
# ─── 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:-}" && -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): run './claude.sh run' and follow the login prompt;"
warn " port 54545 must be reachable from your browser."
2026-04-14 20:11:24 +02:00
fi
}
# ─── Workspace volume resolution ──────────────────────────────────────────────
# Mounts the current working directory as /workspace inside the container.
# Refuses to mount home directories, key material, or system directories.
2026-04-14 20:11:24 +02:00
workspace_flag() {
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
)
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."
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."
exit 1
fi
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."
exit 1
fi
done
echo "--volume ${cwd}:/workspace:z"
2026-04-14 20:11:24 +02:00
}
# ─── 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
fi
echo "--volume ${kube_dir}:/home/node/.kube:ro,z"
}
2026-04-14 20:11:24 +02:00
# ─── 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 --service-ports $(workspace_flag) $(kube_flag) claude "$@"
2026-04-14 20:11:24 +02:00
}
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 --service-ports $(workspace_flag) $(kube_flag) claude "$@"
2026-04-14 20:11:24 +02:00
}
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 --service-ports --entrypoint /bin/bash $(workspace_flag) $(kube_flag) claude
2026-04-14 20:11:24 +02:00
}
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
info "Building images..."
dc build
info "Starting proxy and web interface..."
dc up -d webui
local port=7681
info "Web interface is up → http://0.0.0.0:${port}"
info "Credentials: ${WEBUI_USER:-claude} / [WEBUI_PASSWORD]"
warn "To reach it from outside this host, publish the port:"
warn " sbx ports <sandbox-name> --publish ${port}:${port}/tcp"
}
cmd_web_stop() {
check_deps
info "Stopping web interface..."
dc stop webui
dc rm -f webui
}
2026-04-14 20:11:24 +02:00
cmd_help() {
cat <<EOF
Usage: $(basename "$0") <command> [args]
Commands:
start [args] Build images, start proxy, launch Claude Code (CLI)
run [args] Start proxy if needed, launch Claude Code (CLI)
web Build images, start proxy + web interface (browser terminal)
web-stop Stop the web interface (keeps proxy running)
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 (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.
2026-04-14 20:11:24 +02:00
Flags (before the subcommand):
--kube Mount \$HOME/.kube read-only at /home/node/.kube (kubectl access)
2026-04-14 20:11:24 +02:00
Examples:
cd ~/myproject && ./claude.sh start
cd ~/myproject && ./claude.sh --kube start
./claude.sh web
2026-04-14 20:11:24 +02:00
./claude.sh logs proxy
./claude.sh logs webui
2026-04-14 20:11:24 +02:00
./claude.sh shell
EOF
}
# ─── Dispatch ─────────────────────────────────────────────────────────────────
# Parse global flags before the subcommand
while [[ "${1:-}" == --* ]]; do
case "$1" in
--kube) ALLOW_KUBE=1; shift ;;
*) break ;;
esac
done
2026-04-14 20:11:24 +02:00
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 ;;
2026-04-14 20:11:24 +02:00
*)
error "Unknown command: ${1}"
cmd_help
exit 1
;;
esac