refactor(claude.sh): use array for volume args, merge run into start, tighten helpers
This commit is contained in:
parent
a5af0a5427
commit
3f91b27c94
1 changed files with 63 additions and 125 deletions
188
claude.sh
Normal file → Executable file
188
claude.sh
Normal file → Executable 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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue