diff --git a/.dockerignore b/.dockerignore index ba76e9d..fc75c26 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,5 @@ .env +.npmrc *.log .git README.md diff --git a/.env.example b/.env.example index d8a6d6f..ad79c7b 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,34 @@ # Copy this file to .env and fill in your values. # .env is git-ignored — never commit it. -# Required: your Anthropic API key -ANTHROPIC_API_KEY=sk-ant-... +# ─── Image version ──────────────────────────────────────────────────────────── -# Optional: mount a host directory as /workspace inside the Claude container. -# If unset, a named Docker volume is used (fully isolated from the host). -# WORKSPACE_DIR=/absolute/path/to/your/project +# Pin to a specific image tag. Defaults to "latest" if unset. +# IMAGE_TAG=0.1.42 + +# ─── Authentication (choose one) ────────────────────────────────────────────── + +# Option 1: Anthropic API key +# ANTHROPIC_API_KEY=sk-ant-... + +# Option 2: OAuth token from a Claude.ai subscription (1-year validity) +# Generate with: claude setup-token (run on your host, not inside the container) +# CLAUDE_CODE_OAUTH_TOKEN=... + +# Option 3: No key set — Claude Code will prompt for browser login on first run. +# Port 54545 must be reachable from your browser for the OAuth callback. +# Run: sbx ports --publish 54545:54545/tcp + +# ─── MCP servers (all optional) ─────────────────────────────────────────────── + +# GitHub — PAT with repo scope +# GITHUB_TOKEN=ghp_... + +# GitLab — PAT with api scope; GITLAB_URL defaults to https://gitlab.com +# GITLAB_TOKEN=glpat_... +# GITLAB_URL=https://gitlab.com + +# Jira + Confluence — shared Atlassian credentials +# ATLASSIAN_SITE_NAME=your-company # subdomain of .atlassian.net +# ATLASSIAN_USER_EMAIL=you@example.com +# ATLASSIAN_API_TOKEN=... # https://id.atlassian.com/manage-profile/security/api-tokens diff --git a/.forgejo/workflows/docker-build.yml b/.forgejo/workflows/docker-build.yml new file mode 100644 index 0000000..b13214f --- /dev/null +++ b/.forgejo/workflows/docker-build.yml @@ -0,0 +1,170 @@ +name: Build images + +on: + push: + branches: + - main + +env: + # Set this to the public IP or hostname of your registry, + # whichever you use to reach it from your desktop/laptop + FORGEJO_HOST: code.zeidler.dev + HELM_EXPERIMENTAL_OCI: 1 + TRIVY_IMAGE: registry.zeidler.dev/docker-hub/aquasec/trivy:0.70.0 + GRYPE_IMAGE: registry.zeidler.dev/docker-hub/anchore/grype:v0.88.0 + +jobs: + check-docker: + runs-on: docker-cli + services: + docker: + image: registry.zeidler.dev/docker-hub/catthehacker/ubuntu:act-latest + options: --privileged + container: + image: registry.zeidler.dev/docker-hub/catthehacker/ubuntu:act-latest + steps: + - name: Wait for Docker daemon + run: | + timeout=300 # Set a timeout value in seconds + until docker info; do + echo "Waiting for Docker daemon to start..." + sleep 5 + timeout=$((timeout-5)) + if [ $timeout -le 0 ]; then + echo "Timeout waiting for Docker daemon to start." + exit 1 + fi + done + + scan: + needs: check-docker + runs-on: docker-cli + services: + docker: + image: registry.zeidler.dev/docker-hub/catthehacker/ubuntu:act-latest + options: --privileged + container: + image: registry.zeidler.dev/docker-hub/catthehacker/ubuntu:act-latest + steps: + - name: Checkout the repo + uses: actions/checkout@v4 + + - name: Build proxy image for scanning + run: docker build -t scan/proxy:latest ./proxy + + - name: Generate proxy SBOM + run: | + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$PWD":/output \ + ${{ env.TRIVY_IMAGE }} \ + image --exit-code 0 --vuln-type os,library \ + --format cyclonedx --output /output/sbom-proxy.cdx.json \ + scan/proxy:latest + + - name: Scan proxy image (Trivy) + run: | + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + ${{ env.TRIVY_IMAGE }} \ + image --exit-code 1 --severity HIGH,CRITICAL \ + --ignore-unfixed --vuln-type os,library \ + --format table \ + scan/proxy:latest + + - name: Scan proxy image (Grype) + run: | + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + ${{ env.GRYPE_IMAGE }} \ + docker:scan/proxy:latest \ + --fail-on high \ + --only-fixed + + - name: Build claude image for scanning + run: docker build -t scan/claude:latest ./claude + + - name: Generate claude SBOM + run: | + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$PWD":/output \ + ${{ env.TRIVY_IMAGE }} \ + image --exit-code 0 --vuln-type os,library \ + --format cyclonedx --output /output/sbom-claude.cdx.json \ + scan/claude:latest + + - name: Scan claude image (Trivy) + run: | + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + ${{ env.TRIVY_IMAGE }} \ + image --exit-code 1 --severity HIGH,CRITICAL \ + --ignore-unfixed --vuln-type os,library \ + --format table \ + scan/claude:latest + + - name: Scan claude image (Grype) + run: | + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + ${{ env.GRYPE_IMAGE }} \ + docker:scan/claude:latest \ + --fail-on high \ + --only-fixed + + - name: Upload SBOMs + if: always() + uses: actions/upload-artifact@v4 + with: + name: sboms-${{ env.GITHUB_RUN_NUMBER }} + path: | + sbom-proxy.cdx.json + sbom-claude.cdx.json + retention-days: 90 + + build-and-push: + needs: scan + runs-on: docker-cli + services: + docker: + image: registry.zeidler.dev/docker-hub/catthehacker/ubuntu:act-latest + options: --privileged + environment: deploy + container: + image: registry.zeidler.dev/docker-hub/catthehacker/ubuntu:act-latest + steps: + - name: Checkout the repo + uses: actions/checkout@v4 + - name: Login to the registry + uses: docker/login-action@v3 + with: + registry: ${{ vars.REGISTRY_URL }} + username: ${{ vars.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_PASSWORD }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver: docker-container + - name: Docker publish proxy + uses: docker/build-push-action@v6 + with: + context: proxy + push: true + sbom: true + provenance: true + platforms: linux/amd64, linux/arm64 + tags: | + ${{ vars.REGISTRY_URL }}/docker-public/${{ env.GITHUB_REPOSITORY }}-proxy:0.1.${{ env.GITHUB_RUN_NUMBER }} + ${{ vars.REGISTRY_URL }}/docker-public/${{ env.GITHUB_REPOSITORY }}-proxy:latest + - name: Docker publish claude + uses: docker/build-push-action@v6 + with: + context: claude + push: true + sbom: true + provenance: true + platforms: linux/amd64, linux/arm64 + tags: | + ${{ vars.REGISTRY_URL }}/docker-public/${{ env.GITHUB_REPOSITORY }}-claude:0.1.${{ env.GITHUB_RUN_NUMBER }} + ${{ vars.REGISTRY_URL }}/docker-public/${{ env.GITHUB_REPOSITORY }}-claude:latest diff --git a/CLAUDE.md b/CLAUDE.md index 9af5a32..3e394b2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,40 +10,61 @@ This file provides context and guidance for working with this project. Two containers managed by Docker Compose: -- **`claude`** — Claude Code CLI, non-root (UID 1000), isolated to an internal-only Docker network -- **`proxy`** — Squid forward proxy, non-root (UID 13), bridges the internal network to the internet with an egress allowlist +- **`claude`** — Claude Code CLI (`node:24-alpine`), runs as the built-in `node` user (UID 1000), isolated to an internal-only Docker network +- **`proxy`** — Squid forward proxy (`alpine:3.21`), `squid` user, bridges the internal network to the internet with an egress allowlist Key Docker network property: `claude-internal` has `internal: true`, meaning Docker adds no default gateway. The `claude` container physically cannot reach the internet without going through the `proxy` container. +Auth supports three modes (checked at startup by `claude.sh`): +- `ANTHROPIC_API_KEY` — API key +- `CLAUDE_CODE_OAUTH_TOKEN` — 1-year token from `claude setup-token` (headless-friendly) +- Neither set — Claude Code prompts for browser login on first run; port 54545 is published for the OAuth callback. Credentials persist in `~/.claude` on the host. + ## File Structure ``` docker-claude/ -├── claude.sh # Control script: start / stop / run / update / logs / status / shell -├── docker-compose.yml # Service definitions and network topology -├── Dockerfile.claude # Claude Code container (node:20-slim, UID 1000) -├── Dockerfile.proxy # Squid proxy sidecar (ubuntu:22.04, UID 13) +├── claude.sh # Control script: start/stop/update/logs/status/shell +├── setup.sh # First-time setup wizard (Docker check + auth config) +├── launch.sh # Folder-picker launcher for macOS/Linux +├── launch.bat # Folder-picker launcher for Windows +├── build.sh # Build images locally (development) +├── docker-compose.yml # Service definitions and network topology +├── claude/ +│ └── Dockerfile # Claude Code stable release (node:24-alpine, UID 1000) ├── proxy/ -│ └── squid.conf # Squid ACL config — egress allowlist lives here -├── .env.example # Template for ANTHROPIC_API_KEY -├── .gitignore # Excludes .env and logs -├── .dockerignore # Keeps .env out of build context -└── README.md # User documentation +│ ├── Dockerfile # Squid proxy sidecar (alpine:3.21, squid user) +│ └── squid.conf # Squid ACL config — egress allowlist lives here +├── hooks/ +│ └── pre-commit # Enforces executable bit on shell scripts +├── .env.example # Template for credentials and options +├── .gitignore # Excludes .env and logs +├── .dockerignore # Keeps .env out of build context +└── README.md # User documentation ``` ## Development Workflow ```bash -chmod +x claude.sh -cp .env.example .env # set ANTHROPIC_API_KEY -./claude.sh start # build + start proxy + launch Claude interactively -./claude.sh update # rebuild images (no cache) after upstream updates +./setup.sh # first-time: configure Docker check + auth +cd /path/to/project && ./claude.sh start # start proxy + launch Claude (pulls images, mounts CWD) +./claude.sh update # pull latest images from registry +./build.sh # build images locally (development) +``` + +## Git Hooks + +A pre-commit hook lives in `hooks/` and enforces the executable bit on all shell scripts. Activate it once after cloning: + +```bash +git config core.hooksPath hooks ``` ## Coding Standards - Shell scripts use `set -euo pipefail` -- Dockerfiles use `--no-install-recommends` and clean apt caches in the same layer +- Dockerfiles use Alpine (`node:24-alpine`, `alpine:3.21`) for minimal attack surface +- Alpine packages use `apk add --no-cache`; no apt cache cleanup layer needed - No capabilities granted; `no-new-privileges` on all containers - `.env` is never committed (enforced by `.gitignore` and `.dockerignore`) - Commit messages follow **Angular format**: `type(scope): summary` diff --git a/Dockerfile.claude b/Dockerfile.claude deleted file mode 100644 index ca847d5..0000000 --- a/Dockerfile.claude +++ /dev/null @@ -1,30 +0,0 @@ -FROM node:20-slim - -# Install minimal runtime dependencies -RUN apt-get update && apt-get install -y --no-install-recommends \ - git \ - curl \ - ca-certificates \ - bash \ - && rm -rf /var/lib/apt/lists/* - -# Create non-root user -RUN groupadd -g 1000 claude \ - && useradd -u 1000 -g claude -m -s /bin/bash claude - -# Install Claude Code globally (runs as root for npm -g, then drops) -RUN npm install -g @anthropic-ai/claude-code - -# Workspace directory owned by claude user -RUN mkdir -p /workspace && chown claude:claude /workspace - -USER claude -WORKDIR /workspace - -# Proxy traffic through sidecar — override at runtime if needed -ENV HTTP_PROXY=http://proxy:3128 -ENV HTTPS_PROXY=http://proxy:3128 -ENV ALL_PROXY=http://proxy:3128 -ENV NO_PROXY=localhost,127.0.0.1 - -ENTRYPOINT ["claude"] diff --git a/Dockerfile.proxy b/Dockerfile.proxy deleted file mode 100644 index cfec906..0000000 --- a/Dockerfile.proxy +++ /dev/null @@ -1,25 +0,0 @@ -FROM ubuntu:22.04 - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update && apt-get install -y --no-install-recommends \ - squid \ - && rm -rf /var/lib/apt/lists/* - -# Give the proxy system user (UID 13) ownership of all Squid paths -RUN mkdir -p /var/spool/squid /var/log/squid \ - && chown -R proxy:proxy /var/spool/squid /var/log/squid /etc/squid - -COPY --chown=proxy:proxy proxy/squid.conf /etc/squid/squid.conf - -USER proxy - -# Initialise cache directories as the proxy user -RUN squid -N -f /etc/squid/squid.conf -z 2>/dev/null || true - -EXPOSE 3128 - -HEALTHCHECK --interval=10s --timeout=5s --retries=3 \ - CMD /bin/bash -c 'echo >/dev/tcp/127.0.0.1/3128' - -CMD ["squid", "-N", "-f", "/etc/squid/squid.conf"] diff --git a/README.md b/README.md index dde60b1..af10e9d 100644 --- a/README.md +++ b/README.md @@ -1,120 +1,176 @@ # docker-claude -Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment with a proxy sidecar for controlled egress. Claude cannot reach the host filesystem or network directly. +Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment with a proxy sidecar for controlled egress. Claude cannot access the host filesystem or network directly. + +## Quick Start + +**1. Install a Docker runtime** + +Pick the free, open-source option for your platform: + +| Platform | Recommended | Alternative | +|---|---|---| +| macOS | [Rancher Desktop](https://rancherdesktop.io/) (GUI) | [Colima](https://github.com/abiosoft/colima) (CLI): `brew install colima docker docker-compose && colima start` | +| Linux | Docker Engine: `curl -fsSL https://get.docker.com \| sh` | [Rancher Desktop](https://rancherdesktop.io/) | +| Windows | [Rancher Desktop](https://rancherdesktop.io/) (GUI) | WSL2 + Docker Engine (see below) | + +> **Note:** Docker Desktop is not listed — it requires a commercial licence for business use. + +**2. Download this repo** + +Clone or download and unzip this repository somewhere on your machine. + +**3. Run setup** + +- **macOS / Linux:** Open a terminal, navigate to the folder, and run: + ```bash + ./setup.sh + ``` +- **Windows:** Double-click `launch.bat` — it will run setup automatically on first launch. + +Setup will ask how you want to authenticate (API key, subscription token, or browser login) and save your settings. + +**4. Start Claude** + +- **macOS / Linux:** Double-click `launch.sh`, or run it from a terminal: + ```bash + ./launch.sh + ``` + A folder picker will appear — select the project you want Claude to work on. + +- **Windows:** Double-click `launch.bat`. + +--- ## Architecture ``` -┌─────────────────────────────────────────────────────┐ -│ Host machine │ -│ │ -│ claude.sh (control script) │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ Docker: claude-secure │ │ -│ │ │ │ -│ │ ┌─────────────┐ claude-internal │ │ -│ │ │ claude │◄─────(internal only)───► │ │ -│ │ │ (UID 1000) │ │ │ │ -│ │ └─────────────┘ ┌──────┴──────┐ │ │ -│ │ │ proxy │ │ │ -│ │ │ (UID 13) │ │ │ -│ │ └──────┬──────┘ │ │ -│ │ proxy-external │ │ -│ └─────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ internet (allowlisted) │ -└─────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────┐ +│ Host machine │ +│ │ +│ claude.sh (control script) │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Docker: claude-secure │ │ +│ │ │ │ +│ │ ┌─────────────┐ claude-internal │ │ +│ │ │ claude │ (internal: true) │ │ +│ │ │ (UID 1000) │──────────────► ┌──────────┐ │ │ +│ │ └─────────────┘ │ proxy │ │ │ +│ │ │ (UID 13) │ │ │ +│ │ └────┬─────┘ │ │ +│ │ proxy-external │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ internet (allowlisted) │ +└──────────────────────────────────────────────────────────┘ ``` -- **`claude` container** — Claude Code, runs as UID 1000, on `claude-internal` only (no internet route) -- **`proxy` container** — Squid forward proxy, runs as UID 13, bridges `claude-internal` ↔ internet, enforces egress allowlist -- **`claude-internal`** — Docker bridge with `internal: true`; Docker adds no default gateway, so containers on this network cannot reach the internet directly -- **`proxy-external`** — Standard bridge; the proxy sidecar uses this for controlled outbound access +- **`claude`** — Claude Code CLI (`node:24-alpine`), runs as the built-in `node` user (UID 1000), on `claude-internal` only +- **`proxy`** — Squid forward proxy (`alpine:3.21`), bridges `claude-internal` ↔ internet with egress allowlist +- **`claude-internal`** — `internal: true`; no default gateway, containers cannot reach the internet directly +- **`proxy-external`** — Standard bridge; proxy sidecar only ## Prerequisites -- Docker Engine 24+ -- Docker Compose v2 plugin (`docker compose version`) +A Docker runtime with Compose support. Choose a free, open-source option: -## Setup +- **macOS:** [Rancher Desktop](https://rancherdesktop.io/) or [Colima](https://github.com/abiosoft/colima) +- **Linux:** [Docker Engine CE](https://docs.docker.com/engine/install/) (`curl -fsSL https://get.docker.com | sh`) +- **Windows:** [Rancher Desktop](https://rancherdesktop.io/) or WSL2 + Docker Engine +> Docker Desktop is not recommended — it requires a commercial licence for business use. + +## Authentication + +Three options — `./setup.sh` will guide you through picking one: + +### Option 1 — API key ```bash -# 1. Clone / copy this repo -git clone docker-claude && cd docker-claude - -# 2. Configure your API key -cp .env.example .env -$EDITOR .env # set ANTHROPIC_API_KEY - -# 3. Make the control script executable -chmod +x claude.sh +ANTHROPIC_API_KEY=sk-ant-... ``` +Get a key at [console.anthropic.com](https://console.anthropic.com/settings/keys). + +### Option 2 — OAuth token (subscription, headless-friendly) + +Run this **on your host** (not inside the container) to generate a 1-year token: +```bash +claude setup-token +``` +Then paste the token into setup, or set it manually in `.env`: +```bash +CLAUDE_CODE_OAUTH_TOKEN=... +``` + +### Option 3 — Browser OAuth (interactive) + +Leave both keys unset. On first run, Claude Code will print a login URL. +Port 54545 must be reachable from your browser for the OAuth callback. ## Usage +### Normal use + ```bash -# Build images, start proxy, launch Claude Code interactively +./launch.sh # folder picker → starts Claude in the selected directory +``` + +### CLI / power users + +```bash +cd ~/myproject ./claude.sh start -# Same as start but skips image rebuild (faster on subsequent runs) -./claude.sh run - -# Stop and remove all containers (proxy + any running sessions) -./claude.sh stop - -# Rebuild images without cache (e.g. after Claude Code updates) -./claude.sh update - -# Tail proxy access logs -./claude.sh logs - -# Show container status -./claude.sh status - -# Open a debug bash shell inside the Claude container -./claude.sh shell +./claude.sh stop # Stop and remove all containers +./claude.sh update # Pull latest images from the registry +./claude.sh logs # Tail proxy logs +./claude.sh status # Show container status +./claude.sh shell # Debug bash shell in the Claude container ``` -### Working with host files +### Windows: WSL2 + Docker Engine (alternative to Rancher Desktop) -By default, Claude's workspace is a named Docker volume (`claude-secure-workspace`) — fully isolated from the host. +1. Install [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install): `wsl --install` in PowerShell +2. Open the Ubuntu terminal and run: + ```bash + curl -fsSL https://get.docker.com | sh + sudo usermod -aG docker $USER + ``` +3. Log out and back in, then run `launch.bat` as usual. -To mount a specific host directory: +### Building locally ```bash -WORKSPACE_DIR=$HOME/myproject ./claude.sh run +./build.sh # build with layer cache +./build.sh --no-cache # force full rebuild ``` -The directory is mounted at `/workspace` inside the container. - ## Egress allowlist Edit `proxy/squid.conf` and add domains to the `allowed_sites` ACL: -```squid +``` acl allowed_sites dstdomain api.anthropic.com acl allowed_sites dstdomain statsig.anthropic.com -# acl allowed_sites dstdomain api.github.com # uncomment if needed +# acl allowed_sites dstdomain api.github.com # acl allowed_sites dstdomain registry.npmjs.org ``` -Rebuild the proxy after changes: +Rebuild after changes: ```bash -docker compose -p claude-secure build proxy ./claude.sh stop && ./claude.sh start ``` ## Security controls -| Control | Claude container | Proxy container | +| Control | claude | proxy | |---|---|---| -| Non-root user | UID 1000 (`claude`) | UID 13 (`proxy`) | +| Non-root user | UID 1000 (`node`, built into base image) | `squid` user | | `no-new-privileges` | yes | yes | | All capabilities dropped | yes | yes | | Direct internet access | no (`internal` network only) | allowlisted only | -| Host filesystem | no mounts by default | none | +| Host filesystem | CWD mounted as `/workspace` | none | | Docker socket | not mounted | not mounted | diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..fe5f818 --- /dev/null +++ b/build.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# build.sh — Build Docker images locally for development +# Usage: ./build.sh [docker build flags, e.g. --no-cache] +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REGISTRY="registry.zeidler.dev/docker-public/playground" +TAG="${IMAGE_TAG:-latest}" + +GREEN='\033[0;32m'; NC='\033[0m' +info() { echo -e "${GREEN}[+]${NC} $*"; } + +info "Building proxy..." +docker build "$@" -t "${REGISTRY}/docker-claude-proxy:${TAG}" "${SCRIPT_DIR}/proxy" + +info "Building claude..." +docker build "$@" -t "${REGISTRY}/docker-claude-claude:${TAG}" "${SCRIPT_DIR}/claude" + +info "Done. Run './claude.sh start' to launch." diff --git a/claude.sh b/claude.sh index 52bcc3a..21bede4 100755 --- a/claude.sh +++ b/claude.sh @@ -1,11 +1,12 @@ #!/usr/bin/env bash # claude.sh — Manage the isolated Claude Code Docker environment -# Usage: ./claude.sh [args] +# Usage: ./claude.sh [--kube] [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' @@ -13,96 +14,110 @@ info() { echo -e "${GREEN}[+]${NC} $*"; } warn() { echo -e "${YELLOW}[!]${NC} $*"; } error() { echo -e "${RED}[-]${NC} $*" >&2; } -# ─── Dependency check ───────────────────────────────────────────────────────── +# ─── Helpers ────────────────────────────────────────────────────────────────── check_deps() { if ! command -v docker &>/dev/null; then - error "Docker is not installed. https://docs.docker.com/get-docker/" + 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 v2 plugin is required." + error "Docker Compose is not available. Run ./setup.sh for install instructions." 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." + if [[ ! -f "$env_file" ]]; then + warn "Not set up yet. Run ./setup.sh first." 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" + # 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 } -# ─── Compose wrapper ────────────────────────────────────────────────────────── +# Wrapper so every docker compose call uses the right file and project name. dc() { docker compose -f "$COMPOSE_FILE" -p "$PROJECT" "$@"; } -# ─── Commands ───────────────────────────────────────────────────────────────── +# ─── 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 - info "Building images..." - dc build + check_deps; load_env; build_volume_args + info "Pulling latest images..." + dc pull 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 "$@" + dc run --rm --service-ports "${VOLUME_ARGS[@]}" 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 "Pulling latest images from registry..." + dc pull info "Update complete. Run './claude.sh start' to launch." } cmd_logs() { check_deps - local svc="${1:-proxy}" - dc logs -f "$svc" + dc logs -f "${1:-proxy}" } cmd_status() { @@ -111,50 +126,56 @@ cmd_status() { } cmd_shell() { - check_deps - load_env + check_deps; load_env; build_volume_args warn "Opening debug shell inside Claude container (non-Claude entrypoint)." - # shellcheck disable=SC2046 - dc run --rm --entrypoint /bin/bash $(workspace_flag) claude + dc run --rm --service-ports --entrypoint /bin/bash "${VOLUME_ARGS[@]}" claude } cmd_help() { cat < [args] +Usage: $(basename "$0") [--kube] [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 + 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 -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). +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: - ./claude.sh start - WORKSPACE_DIR=\$HOME/myproject ./claude.sh run + 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) 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 ;; + 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 diff --git a/claude/Dockerfile b/claude/Dockerfile new file mode 100644 index 0000000..6d6c0fd --- /dev/null +++ b/claude/Dockerfile @@ -0,0 +1,107 @@ +FROM node:24-alpine + +# Upgrade npm to pull in patched bundled deps (cross-spawn, glob, minimatch, tar) +# CVEs: CVE-2024-21538, CVE-2025-64756, CVE-2026-26996/27903/27904, CVE-2026-23745/23950/24842/26960/29786/31802 +RUN npm install -g npm@11.12.1 + +# Fix CVE-2026-33671: upgrade picomatch 4.0.3 → 4.0.4 in every location it appears +RUN find /usr/local/lib/node_modules -name "picomatch" -type d | while read dir; do \ + ver=$(node -p "require('$dir/package.json').version" 2>/dev/null); \ + [ "$ver" = "4.0.3" ] || continue; \ + echo "Patching picomatch in $dir"; \ + prefix=$(dirname "$(dirname "$dir")"); \ + npm install --prefix "$prefix" picomatch@4.0.4 \ + --no-save --no-audit --no-fund 2>/dev/null || true; \ + done + +# Install runtime dependencies +RUN apk add --no-cache \ + git \ + curl \ + ca-certificates \ + bash + +# Install kubectl — architecture-aware, checksum-verified +RUN KUBECTL_VERSION=$(curl -fsSL https://dl.k8s.io/release/stable.txt) \ + && ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') \ + && curl -fsSL "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl" \ + -o /usr/local/bin/kubectl \ + && curl -fsSL "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl.sha256" \ + -o /tmp/kubectl.sha256 \ + && echo "$(cat /tmp/kubectl.sha256) /usr/local/bin/kubectl" | sha256sum -c \ + && rm /tmp/kubectl.sha256 \ + && chmod +x /usr/local/bin/kubectl + +# System-level Claude Code policy — owned by root, not writable by the node user. +# Restricts available models; cannot be bypassed via CLI flags or env vars. +COPY settings.json /etc/claude-code/managed-settings.json + +# Install Claude Code stable release +RUN curl -fsSL https://claude.ai/install.sh | bash -s stable + +# Install MCP servers, patch transitive CVEs, scrub credentials and cache — all in one +# layer so nothing is committed to the image between install and cleanup. +# +# CVEs patched: +# CVE-2025-66414, CVE-2026-0621 — @modelcontextprotocol/sdk <1.25.2 +# GHSA-345p-7cg4-v4c7 — @modelcontextprotocol/sdk <1.26.0 +# CVE-2026-33671 — picomatch <4.0.4 +# GHSA-f886-m6hf-6m8v — brace-expansion <5.0.5 +RUN npm install -g \ + @modelcontextprotocol/server-github \ + @yoda.digital/gitlab-mcp-server \ + @aashari/mcp-server-atlassian-jira \ + @aashari/mcp-server-atlassian-confluence \ + && for pkg_dir in \ + /usr/local/lib/node_modules/@modelcontextprotocol/server-github \ + /usr/local/lib/node_modules/@yoda.digital/gitlab-mcp-server \ + /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-jira \ + /usr/local/lib/node_modules/@aashari/mcp-server-atlassian-confluence; do \ + [ -d "$pkg_dir" ] && \ + cd "$pkg_dir" && \ + npm install --no-audit --no-fund \ + @modelcontextprotocol/sdk@1.26.0 \ + picomatch@4.0.4 \ + brace-expansion@5.0.5 \ + || true; \ + done \ + && find /usr/local/lib/node_modules -name "picomatch" -type d | while read dir; do \ + ver=$(node -p "require('$dir/package.json').version" 2>/dev/null); \ + [ "$ver" = "4.0.3" ] || continue; \ + prefix=$(dirname "$(dirname "$dir")"); \ + npm install --prefix "$prefix" picomatch@4.0.4 \ + --no-save --no-audit --no-fund 2>/dev/null || true; \ + done \ + && cd /tmp \ + && npm pack brace-expansion@5.0.5 --no-audit 2>/dev/null \ + && tar xzf brace-expansion-5.0.5.tgz \ + && find /usr/local/lib/node_modules -name "package.json" -path "*/brace-expansion/package.json" \ + | xargs grep -l '"version": "5.0.4"' 2>/dev/null \ + | while read pj; do \ + cp -r /tmp/package/. "$(dirname "$pj")/"; \ + done \ + && rm -rf /tmp/brace-expansion-5.0.5.tgz /tmp/package \ + && find /root /home /usr/local/etc -name ".npmrc" -o -name "npmrc" \ + | xargs grep -l "_authToken\|_auth\b" 2>/dev/null \ + | xargs rm -f 2>/dev/null || true \ + && npm config delete //npm.pkg.github.com/:_authToken 2>/dev/null || true \ + && npm config delete //registry.npmjs.org/:_authToken 2>/dev/null || true \ + && npm cache clean --force + +# Workspace and Claude config dir — owned by the built-in node user (uid 1000). +# Pre-creating ~/.claude ensures the named volume is initialised with the +# correct ownership when first mounted (Docker copies image content into +# an empty named volume on first use). +RUN mkdir -p /workspace /home/node/.claude \ + && chown -R node:node /workspace /home/node/.claude + +USER node +WORKDIR /workspace + +# Proxy traffic through sidecar — override at runtime if needed +ENV HTTP_PROXY=http://proxy:3128 +ENV HTTPS_PROXY=http://proxy:3128 +ENV ALL_PROXY=http://proxy:3128 +ENV NO_PROXY=localhost,127.0.0.1 + +ENTRYPOINT ["claude"] diff --git a/claude/settings.json b/claude/settings.json new file mode 100644 index 0000000..175bdd4 --- /dev/null +++ b/claude/settings.json @@ -0,0 +1,10 @@ +{ + "availableModels": ["sonnet", "opus", "haiku"], + "permissions": { + "allow": ["Bash(*)", "Edit(*)", "Write(*)"], + "deny": ["Bash(curl *)", "Read(.*env*)"], + "env": { + "CLAUDE_CODE_ENABLE_TELEMETRY": "0" + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml index baadf41..4148250 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,12 @@ services: - # ─── Proxy sidecar ───────────────────────────────────────────────────────── # Bridges the isolated internal network to the internet. # Enforces an egress allowlist — see proxy/squid.conf. proxy: - build: - context: . - dockerfile: Dockerfile.proxy + image: registry.zeidler.dev/docker-public/playground/docker-claude-proxy:${IMAGE_TAG:-latest} networks: - - claude-internal # reachable by the claude container - - proxy-external # has outbound internet access + - claude-internal # reachable by claude container + - proxy-external # has outbound internet access restart: unless-stopped security_opt: - no-new-privileges:true @@ -18,35 +15,45 @@ services: read_only: true tmpfs: - /tmp - - /var/spool/squid + - /var/cache/squid - /var/log/squid - # ─── Claude Code container ───────────────────────────────────────────────── + # ─── Claude Code CLI container ───────────────────────────────────────────── # No direct internet access. All egress routes through the proxy sidecar. - # Run via "docker compose run --rm claude" (managed by claude.sh). + # Run via "docker compose run --rm --service-ports claude" (managed by claude.sh). claude: - build: - context: . - dockerfile: Dockerfile.claude + image: registry.zeidler.dev/docker-public/playground/docker-claude-claude:${IMAGE_TAG:-latest} depends_on: proxy: condition: service_healthy networks: - - claude-internal # only — no route to the internet + - claude-internal # only — no route to the internet environment: - - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} + - CLAUDE_CODE_OAUTH_TOKEN=${CLAUDE_CODE_OAUTH_TOKEN:-} - HTTP_PROXY=http://proxy:3128 - HTTPS_PROXY=http://proxy:3128 - ALL_PROXY=http://proxy:3128 - NO_PROXY=localhost,127.0.0.1 + # MCP server credentials — all optional; servers are skipped if unset + - GITHUB_TOKEN=${GITHUB_TOKEN:-} + - GITLAB_TOKEN=${GITLAB_TOKEN:-} + - GITLAB_URL=${GITLAB_URL:-https://gitlab.com} + - ATLASSIAN_SITE_NAME=${ATLASSIAN_SITE_NAME:-} + - ATLASSIAN_USER_EMAIL=${ATLASSIAN_USER_EMAIL:-} + - ATLASSIAN_API_TOKEN=${ATLASSIAN_API_TOKEN:-} + ports: + # OAuth callback — required for browser-based login (claude login) + - "0.0.0.0:54545:54545" + volumes: + - ${HOME}/.claude:/home/node/.claude + # Workspace is injected by claude.sh via --volume flag at run time (current directory). security_opt: - no-new-privileges:true cap_drop: - ALL stdin_open: true tty: true - # Workspace is injected by claude.sh via --volume flag at run time. - # Default: named Docker volume. Override: set WORKSPACE_DIR on the host. networks: # Internal-only: Docker adds no default gateway → no direct internet route @@ -57,3 +64,4 @@ networks: # External: standard bridge with internet access (proxy only) proxy-external: driver: bridge + diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100755 index 0000000..cf9ee40 --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Ensure control scripts stay executable. +set -euo pipefail + +SCRIPTS=(claude.sh build.sh setup.sh launch.sh hooks/pre-commit) + +for f in "${SCRIPTS[@]}"; do + if [[ -f "$f" && ! -x "$f" ]]; then + echo "pre-commit: fixing missing executable bit on $f" + chmod +x "$f" + git add "$f" + fi +done diff --git a/launch.bat b/launch.bat new file mode 100644 index 0000000..21d3d13 --- /dev/null +++ b/launch.bat @@ -0,0 +1,55 @@ +@echo off +:: launch.bat — Pick a project folder and start Claude Code (Windows) +setlocal enabledelayedexpansion + +set "SCRIPT_DIR=%~dp0" +set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%" + +:: ── Check for bash (Git Bash or WSL) ───────────────────────────────────────── +where bash >nul 2>&1 +if %errorlevel% neq 0 ( + echo Git Bash is required to run docker-claude on Windows. + echo. + echo Download it at: https://git-scm.com/download/win + echo Install with default options, then double-click this file again. + pause + exit /b 1 +) + +:: ── First-time setup ────────────────────────────────────────────────────────── +if not exist "%SCRIPT_DIR%\.env" ( + echo Looks like this is your first time. Running setup... + echo. + bash "%SCRIPT_DIR%/setup.sh" + if %errorlevel% neq 0 ( pause & exit /b 1 ) + echo. +) + +:: ── Folder picker via PowerShell ────────────────────────────────────────────── +set "PROJECT_FOLDER=" +for /f "usebackq tokens=*" %%i in (`powershell -NoProfile -Command ^ + "Add-Type -AssemblyName System.Windows.Forms; ^ + $d = New-Object System.Windows.Forms.FolderBrowserDialog; ^ + $d.Description = 'Select the project folder to work on'; ^ + $d.RootFolder = 'MyComputer'; ^ + $d.ShowNewFolderButton = $false; ^ + if ($d.ShowDialog() -eq 'OK') { Write-Output $d.SelectedPath } else { exit 1 }"`) do ( + set "PROJECT_FOLDER=%%i" +) + +if not defined PROJECT_FOLDER ( + echo No folder selected. Exiting. + pause + exit /b 1 +) + +:: ── Launch ──────────────────────────────────────────────────────────────────── +:: Convert Windows path to Unix path for bash +for /f "usebackq tokens=*" %%i in (`bash -c "cygpath -u '!PROJECT_FOLDER!'"`) do ( + set "UNIX_FOLDER=%%i" +) + +bash -c "cd '!UNIX_FOLDER!' && '!SCRIPT_DIR:/=\..\..\!/claude.sh' start" 2>nul || ^ +bash -c "cd '!UNIX_FOLDER!' && bash '$(cygpath -u '!SCRIPT_DIR!')/claude.sh' start" + +pause diff --git a/launch.sh b/launch.sh new file mode 100755 index 0000000..d987957 --- /dev/null +++ b/launch.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# launch.sh — Pick a project folder and start Claude Code +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# ─── First-time setup ───────────────────────────────────────────────────────── +if [[ ! -f "$SCRIPT_DIR/.env" ]]; then + echo "Looks like this is your first time. Running setup..." + echo "" + "$SCRIPT_DIR/setup.sh" || exit 1 + echo "" +fi + +# ─── Folder picker ──────────────────────────────────────────────────────────── +pick_folder() { + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS — native Finder dialog + osascript -e \ + 'tell application "Finder" to POSIX path of (choose folder with prompt "Select the project folder to work on:")' \ + 2>/dev/null | tr -d '\n' + elif command -v zenity &>/dev/null; then + # Linux — GNOME/GTK dialog + zenity --file-selection --directory \ + --title="Select your project folder" 2>/dev/null + elif command -v kdialog &>/dev/null; then + # Linux — KDE dialog + kdialog --getexistingdirectory "$HOME" \ + --title "Select your project folder" 2>/dev/null + else + echo "" + fi +} + +folder=$(pick_folder || true) + +# Fallback: text prompt (no GUI available, or user cancelled dialog) +if [[ -z "$folder" ]]; then + echo "Enter the path to your project folder" + echo "(Tip: you can drag the folder into this window, then press Enter)" + echo "" + read -rp "> " folder + # Clean up: strip surrounding quotes and trailing whitespace from drag-and-drop + folder="${folder%"${folder##*[![:space:]]}"}" + folder="${folder#\'}" ; folder="${folder%\'}" + folder="${folder#\"}" ; folder="${folder%\"}" + # Expand ~ to home directory + folder="${folder/#\~/$HOME}" +fi + +if [[ -z "$folder" ]]; then + echo "No folder selected. Exiting." + exit 1 +fi + +if [[ ! -d "$folder" ]]; then + echo "Folder not found: $folder" + exit 1 +fi + +cd "$folder" +exec "$SCRIPT_DIR/claude.sh" start diff --git a/proxy/Dockerfile b/proxy/Dockerfile new file mode 100644 index 0000000..5d494e6 --- /dev/null +++ b/proxy/Dockerfile @@ -0,0 +1,19 @@ +FROM registry.zeidler.dev/docker-dhi/alpine-base:3.23-alpine3.23-dev + +# squid: proxy. netcat-openbsd: health check +RUN apk add --no-cache squid netcat-openbsd + +# squid user is created by the package (apk add squid) +RUN mkdir -p /var/cache/squid /var/log/squid \ + && chown -R squid:squid /var/cache/squid /var/log/squid /etc/squid + +COPY --chown=squid:squid squid.conf /etc/squid/squid.conf + +USER squid + +EXPOSE 3128 + +HEALTHCHECK --interval=10s --timeout=5s --retries=3 \ + CMD nc -z 127.0.0.1 3128 || exit 1 + +CMD ["squid", "-N", "-f", "/etc/squid/squid.conf"] diff --git a/proxy/squid.conf b/proxy/squid.conf index 6ef039f..4deb96d 100644 --- a/proxy/squid.conf +++ b/proxy/squid.conf @@ -14,18 +14,27 @@ cache_store_log none # ─── No disk cache ──────────────────────────────────────────────────────────── cache deny all -coredump_dir /var/spool/squid +coredump_dir /var/cache/squid # ─── ACL Definitions ────────────────────────────────────────────────────────── acl SSL_ports port 443 +acl SSL_ports port 6443 # Kubernetes API server acl Safe_ports port 80 acl Safe_ports port 443 +acl Safe_ports port 6443 # Kubernetes API server acl CONNECT method CONNECT # ─── Egress allowlist ───────────────────────────────────────────────────────── # Add domains here as needed. Leading dot matches all subdomains. acl allowed_sites dstdomain api.anthropic.com acl allowed_sites dstdomain statsig.anthropic.com +acl allowed_sites dstdomain platform.claude.com +# MCP servers +acl allowed_sites dstdomain api.github.com +acl allowed_sites dstdomain .gitlab.com +acl allowed_sites dstdomain .atlassian.net +# Kubernetes API server — add your cluster's hostname here when using --kube +# acl allowed_sites dstdomain k8s.example.com # ─── Access rules ───────────────────────────────────────────────────────────── # Block requests to non-standard ports diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..da6c37a --- /dev/null +++ b/setup.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# setup.sh — First-time setup wizard for docker-claude +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="$SCRIPT_DIR/.env" + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BOLD='\033[1m'; NC='\033[0m' +info() { echo -e "${GREEN}[+]${NC} $*"; } +warn() { echo -e "${YELLOW}[!]${NC} $*"; } +error() { echo -e "${RED}[✗]${NC} $*" >&2; } +step() { echo -e "\n${BOLD}$*${NC}"; } + +# ─── Platform-specific install hints ───────────────────────────────────────── +docker_install_hint() { + case "$(uname -s)" in + Darwin) + echo " Install one of the following (both are free and open source):" + echo " • Rancher Desktop (GUI, easiest): https://rancherdesktop.io/" + echo " • Colima (CLI): brew install colima docker docker-compose && colima start" + ;; + Linux) + echo " Install Docker Engine (free, no licensing restrictions):" + echo " curl -fsSL https://get.docker.com | sh" + echo " sudo usermod -aG docker \$USER # then log out and back in" + ;; + *) + # Windows / Git Bash / WSL + echo " Install one of the following (both are free and open source):" + echo " • Rancher Desktop (GUI, easiest): https://rancherdesktop.io/" + echo " • WSL2 + Docker Engine: install Ubuntu from the Microsoft Store," + echo " then run: curl -fsSL https://get.docker.com | sh" + ;; + esac +} + +docker_not_running_hint() { + case "$(uname -s)" in + Darwin|MINGW*|MSYS*|CYGWIN*) + echo " → Open Rancher Desktop (or whichever Docker runtime you installed)" + echo " and wait for it to finish starting, then run this setup again." + ;; + Linux) + echo " → Start the Docker daemon: sudo systemctl start docker" + ;; + *) + echo " → Start your Docker runtime and try again." + ;; + esac +} + +# ─── Check Docker ───────────────────────────────────────────────────────────── +check_docker() { + step "Checking Docker..." + + if ! command -v docker &>/dev/null; then + error "Docker is not installed." + docker_install_hint + exit 1 + fi + + if ! docker info &>/dev/null 2>&1; then + error "Docker is installed but not running." + docker_not_running_hint + exit 1 + fi + + if ! docker compose version &>/dev/null 2>&1; then + error "Docker Compose is not available." + echo " Docker Compose is included with Rancher Desktop and Docker Engine." + docker_install_hint + exit 1 + fi + + info "Docker is ready." +} + +# ─── Auth setup ─────────────────────────────────────────────────────────────── +setup_auth() { + step "Authentication" + echo " How would you like to sign in to Claude?" + echo "" + echo " 1) Anthropic API key (pay-per-use)" + echo " Get one at: https://console.anthropic.com/settings/keys" + echo "" + echo " 2) Claude subscription (Claude Pro or Max)" + echo " Generates a token from your existing subscription." + echo "" + echo " 3) Browser login (sign in when Claude first starts)" + echo "" + read -rp " Choice [1/2/3, default: 3]: " choice + choice="${choice:-3}" + + case "$choice" in + 1) + echo "" + read -rp " Paste your API key (sk-ant-...): " api_key + if [[ -z "$api_key" ]]; then + error "No API key entered. Run setup again when you have one." + exit 1 + fi + echo "ANTHROPIC_API_KEY=$api_key" > "$ENV_FILE" + ;; + 2) + echo "" + echo " You'll need to run 'claude setup-token' on your host to generate a token." + echo " If Claude Code is installed natively, run that command now and paste the result." + echo " Otherwise choose option 3 (browser login)." + echo "" + read -rp " Paste your OAuth token: " token + if [[ -z "$token" ]]; then + error "No token entered. Run setup again when you have one." + exit 1 + fi + echo "CLAUDE_CODE_OAUTH_TOKEN=$token" > "$ENV_FILE" + ;; + 3) + touch "$ENV_FILE" + warn "Browser login selected." + warn "When Claude starts for the first time, it will print a login URL." + warn "Open that URL in your browser to sign in." + ;; + *) + error "Invalid choice: $choice" + exit 1 + ;; + esac +} + +# ─── Main ───────────────────────────────────────────────────────────────────── +echo -e "\n${BOLD}docker-claude setup${NC}" +echo "────────────────────" + +if [[ -f "$ENV_FILE" ]]; then + warn ".env already exists (setup was already run)." + read -rp " Reconfigure authentication? [y/N]: " confirm + if [[ "${confirm,,}" != "y" ]]; then + info "Setup skipped. Run ./launch.sh to start Claude." + exit 0 + fi +fi + +check_docker +setup_auth + +echo "" +info "Setup complete!" +info "→ Run ./launch.sh to start Claude Code."