diff --git a/.env.example b/.env.example index 00a8187..502aa0d 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,27 @@ # 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-... +# ─── 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 + +# ─── Workspace (CLI mode only) ──────────────────────────────────────────────── # 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 -# Web interface credentials (required for ./claude.sh web) +# ─── Web interface ──────────────────────────────────────────────────────────── + +# Required for ./claude.sh web # WEBUI_USER=claude # WEBUI_PASSWORD=changeme diff --git a/CLAUDE.md b/CLAUDE.md index 547181c..41d07a2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,6 +18,11 @@ Key Docker network property: `claude-internal` has `internal: true`, meaning Doc The `webui` service reuses `claude/Dockerfile`. Its entrypoint (`claude/webui-entrypoint.sh`) starts `ttyd --credential user:pass claude` instead of `claude` directly. +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 the `claude-config` named volume. + ## File Structure ``` diff --git a/README.md b/README.md index 2da158b..769d2f4 100644 --- a/README.md +++ b/README.md @@ -47,14 +47,46 @@ Runs [Claude Code](https://claude.ai/code) inside an isolated Docker environment # 1. Clone / copy this repo git clone docker-claude && cd docker-claude -# 2. Configure credentials +# 2. Configure credentials (see Authentication below) cp .env.example .env -$EDITOR .env # set ANTHROPIC_API_KEY (and WEBUI_PASSWORD if using web mode) +$EDITOR .env # 3. Make the control script executable chmod +x claude.sh ``` +## Authentication + +Three options — pick one and set it in `.env`: + +### Option 1 — API key +```bash +ANTHROPIC_API_KEY=sk-ant-... +``` + +### 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 set the printed token 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: + +```bash +sbx ports --publish 54545:54545/tcp +``` + +Then run `./claude.sh run` and follow the prompt. Credentials are stored in the +`claude-config` Docker volume and reused on every subsequent run. + ## Usage ### CLI mode diff --git a/claude.sh b/claude.sh old mode 100755 new mode 100644 index a302548..de90c7f --- a/claude.sh +++ b/claude.sh @@ -32,10 +32,13 @@ load_env() { # 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." - exit 1 + 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." fi } @@ -72,7 +75,7 @@ cmd_start() { 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 $(workspace_flag) claude "$@" } cmd_stop() { @@ -89,7 +92,7 @@ cmd_run() { dc up -d proxy info "Launching Claude Code..." # shellcheck disable=SC2046 - dc run --rm $(workspace_flag) claude "$@" + dc run --rm --service-ports $(workspace_flag) claude "$@" } cmd_update() { @@ -115,7 +118,7 @@ cmd_shell() { load_env 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 $(workspace_flag) claude } cmd_web() { diff --git a/claude/Dockerfile b/claude/Dockerfile index 42b2cf7..466933b 100644 --- a/claude/Dockerfile +++ b/claude/Dockerfile @@ -18,8 +18,12 @@ RUN addgroup -g 1000 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 +# Workspace and Claude config dir — both owned by claude user. +# 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/claude/.claude \ + && chown -R claude:claude /workspace /home/claude/.claude USER claude WORKDIR /workspace diff --git a/docker-compose.yml b/docker-compose.yml index 8d6c1ab..e71a964 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: # ─── 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: claude/ @@ -33,19 +33,25 @@ services: networks: - 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 + ports: + # OAuth callback — required for browser-based login (claude login) + - "0.0.0.0:54545:54545" + volumes: + - claude-config:/home/claude/.claude + # Workspace is injected by claude.sh via --volume flag at run time. + # Default: named Docker volume. Override: set WORKSPACE_DIR on the host. 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. # ─── Claude Code web interface ───────────────────────────────────────────── # Serves Claude Code as a browser terminal via ttyd (port 7681). @@ -62,7 +68,8 @@ services: networks: - 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 @@ -72,7 +79,10 @@ services: - WEBUI_PORT=7681 ports: - "0.0.0.0:7681:7681" + # OAuth callback — required for browser-based login (claude login) + - "0.0.0.0:54545:54545" volumes: + - claude-config:/home/claude/.claude - claude-web-workspace:/workspace security_opt: - no-new-privileges:true @@ -93,5 +103,8 @@ networks: driver: bridge volumes: + # Persists Claude Code auth credentials (~/.claude/) across container runs. + # Shared between the CLI and web interface so login carries over. + claude-config: # Persistent workspace for the web interface claude-web-workspace: