docker-claude/claude.sh
docker-claude b741b02408 fix(dockerfile): scrub npm auth tokens written during image build
npm automatically picks up GITHUB_TOKEN / NPM_TOKEN from the build
environment and writes them as _authToken entries in /root/.npmrc and
/usr/local/etc/npmrc during 'npm install -g'.  Add a cleanup RUN step
that removes any npmrc file containing auth tokens before the image is
finalised, and explicitly deletes the two most common registry auth
keys via 'npm config delete'.

Also add .npmrc to .dockerignore as an extra guard against accidentally
COPY-ing a local credential file into the build context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 16:37:00 +02:00

184 lines
6.4 KiB
Bash
Executable file

#!/usr/bin/env bash
# claude.sh — Manage the isolated Claude Code Docker environment
# Usage: ./claude.sh [--kube] <command> [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() {
if ! command -v docker &>/dev/null; then
error "Docker is not installed. Run ./setup.sh for install instructions."
exit 1
fi
if ! docker info &>/dev/null 2>&1; then
error "Docker is not running. Start your Docker runtime, then try again."
exit 1
fi
if ! docker compose version &>/dev/null 2>&1; then
error "Docker Compose is not available. Run ./setup.sh for install instructions."
exit 1
fi
}
load_env() {
local env_file="$SCRIPT_DIR/.env"
if [[ ! -f "$env_file" ]]; then
warn "Not set up yet. Run ./setup.sh first."
exit 1
fi
# shellcheck disable=SC1090
set -a; source "$env_file"; set +a
if [[ -z "${ANTHROPIC_API_KEY:-}" && -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then
warn "No credentials found — Claude will ask you to log in via browser."
warn "A login URL will appear below. Open it to authenticate."
warn "(To skip this prompt in future, run ./setup.sh to configure credentials.)"
echo ""
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 <<EOF
Usage: $(basename "$0") [--kube] <command> [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