refactor(claude.sh): use array for volume args, merge run into start, tighten helpers

This commit is contained in:
docker-claude 2026-04-15 17:14:37 +02:00
parent a5af0a5427
commit 3f91b27c94

188
claude.sh Normal file → Executable file
View file

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