|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Wrapper to run Claude Code inside a Docker container with |
| 3 | +# --dangerously-skip-permissions. The repo is volume-mounted so all |
| 4 | +# edits are reflected on the host. |
| 5 | +# |
| 6 | +# Usage: |
| 7 | +# ./claude.sh --build # build the Docker image (one-time) |
| 8 | +# ./claude.sh # start Claude Code interactively |
| 9 | +# ./claude.sh --shell # get a bash shell instead |
| 10 | +# ./claude.sh -- -p "prompt" # pass extra args to claude |
| 11 | + |
| 12 | +set -euo pipefail |
| 13 | + |
| 14 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 15 | + |
| 16 | +# --- Defaults (overridable via environment) --- |
| 17 | +CLAUDE_IMAGE="${CLAUDE_IMAGE:-openroad/flow-claude:latest}" |
| 18 | +CONTAINER_NAME="${CONTAINER_NAME:-claude-orfs}" |
| 19 | + |
| 20 | +# --- Argument parsing --- |
| 21 | +SHELL_MODE=0 |
| 22 | +BUILD_IMAGE=0 |
| 23 | +CLAUDE_ARGS=() |
| 24 | + |
| 25 | +usage() { |
| 26 | + cat <<EOF |
| 27 | +Usage: $0 [OPTIONS] [-- CLAUDE_ARGS...] |
| 28 | +
|
| 29 | +Options: |
| 30 | + --build Build the Docker image |
| 31 | + --shell Start a bash shell instead of Claude Code |
| 32 | + --image NAME Override Docker image (default: ${CLAUDE_IMAGE}) |
| 33 | + --name NAME Override container name (default: ${CONTAINER_NAME}) |
| 34 | + -h, --help Show this help |
| 35 | +
|
| 36 | +Environment: |
| 37 | + CLAUDE_IMAGE Docker image override (same as --image). |
| 38 | + CONTAINER_NAME Container name override (same as --name). |
| 39 | + ANTHROPIC_API_KEY Optional. Passed through if set. |
| 40 | +
|
| 41 | +Examples: |
| 42 | + $0 --build # build the image |
| 43 | + $0 # interactive Claude Code |
| 44 | + $0 -- -p "fix the floorplan" # Claude with a prompt |
| 45 | + $0 --shell # bash inside the container |
| 46 | +EOF |
| 47 | + exit 0 |
| 48 | +} |
| 49 | + |
| 50 | +while [[ $# -gt 0 ]]; do |
| 51 | + case "$1" in |
| 52 | + --build) BUILD_IMAGE=1; shift ;; |
| 53 | + --shell) SHELL_MODE=1; shift ;; |
| 54 | + --image) CLAUDE_IMAGE="$2"; shift 2 ;; |
| 55 | + --name) CONTAINER_NAME="$2"; shift 2 ;; |
| 56 | + -h|--help) usage ;; |
| 57 | + --) shift; CLAUDE_ARGS=("$@"); break ;; |
| 58 | + *) CLAUDE_ARGS+=("$1"); shift ;; |
| 59 | + esac |
| 60 | +done |
| 61 | + |
| 62 | +# --- Build image (explicitly or automatically if missing) --- |
| 63 | +_build_image() { |
| 64 | + echo "Building Claude Code Docker image: ${CLAUDE_IMAGE}" |
| 65 | + docker build \ |
| 66 | + -f "${SCRIPT_DIR}/docker/Dockerfile.claude" \ |
| 67 | + -t "${CLAUDE_IMAGE}" \ |
| 68 | + "${SCRIPT_DIR}/docker" |
| 69 | + echo "Done. Image: ${CLAUDE_IMAGE}" |
| 70 | +} |
| 71 | + |
| 72 | +if [[ "${BUILD_IMAGE}" -eq 1 ]]; then |
| 73 | + _build_image |
| 74 | + # If only --build was given, exit. |
| 75 | + if [[ "${SHELL_MODE}" -eq 0 ]] && [[ ${#CLAUDE_ARGS[@]} -eq 0 ]]; then |
| 76 | + exit 0 |
| 77 | + fi |
| 78 | +elif ! docker image inspect "${CLAUDE_IMAGE}" > /dev/null 2>&1; then |
| 79 | + echo "Image ${CLAUDE_IMAGE} not found. Building automatically..." |
| 80 | + _build_image |
| 81 | +fi |
| 82 | + |
| 83 | +# --- Determine the user's home inside the container --- |
| 84 | +# The entrypoint creates /home/claude-user for the mapped user. |
| 85 | +CONTAINER_HOME="/home/claude-user" |
| 86 | + |
| 87 | +# --- Assemble docker run arguments --- |
| 88 | +DOCKER_RUN_ARGS=( |
| 89 | + --rm |
| 90 | + --name "${CONTAINER_NAME}" |
| 91 | + -v "${SCRIPT_DIR}:/workspace" |
| 92 | + -e "HOST_UID=$(id -u)" |
| 93 | + -e "HOST_GID=$(id -g)" |
| 94 | + -w /workspace |
| 95 | +) |
| 96 | + |
| 97 | +# TTY/stdin handling for interactive, CI, and piped-input cases: |
| 98 | +# * terminal in + terminal out → -it (normal interactive use) |
| 99 | +# * anything else → -i (pass stdin through; no TTY) |
| 100 | +# Forcing -it unconditionally breaks non-interactive use |
| 101 | +# ("cannot attach stdin to a TTY-enabled container"), and dropping -i |
| 102 | +# would silently discard piped input like `cat prompt.txt | claude.sh ...`. |
| 103 | +if [[ -t 0 && -t 1 ]]; then |
| 104 | + DOCKER_RUN_ARGS+=(-it) |
| 105 | +else |
| 106 | + DOCKER_RUN_ARGS+=(-i) |
| 107 | +fi |
| 108 | + |
| 109 | +# Pass through optional environment variables |
| 110 | +for var in ANTHROPIC_API_KEY ANTHROPIC_MODEL CLAUDE_CODE_MAX_TURNS \ |
| 111 | + CLAUDE_CODE_USE_BEDROCK \ |
| 112 | + AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_REGION \ |
| 113 | + GOOGLE_APPLICATION_CREDENTIALS; do |
| 114 | + if [[ -v "$var" ]] && [[ -n "${!var}" ]]; then |
| 115 | + DOCKER_RUN_ARGS+=(-e "${var}=${!var}") |
| 116 | + fi |
| 117 | +done |
| 118 | + |
| 119 | +# SSH agent forwarding (for git push/pull over SSH) |
| 120 | +if [[ -n "${SSH_AUTH_SOCK:-}" ]]; then |
| 121 | + DOCKER_RUN_ARGS+=( |
| 122 | + -v "${SSH_AUTH_SOCK}:/tmp/ssh-agent.sock" |
| 123 | + -e "SSH_AUTH_SOCK=/tmp/ssh-agent.sock" |
| 124 | + ) |
| 125 | +fi |
| 126 | + |
| 127 | +# Host gitconfig (read-only, for user.name / user.email) |
| 128 | +if [[ -f "${HOME}/.gitconfig" ]]; then |
| 129 | + DOCKER_RUN_ARGS+=(-v "${HOME}/.gitconfig:${CONTAINER_HOME}/.gitconfig:ro") |
| 130 | +fi |
| 131 | + |
| 132 | +# Persist Claude Code settings / history / memory across runs |
| 133 | +CLAUDE_CONFIG_DIR="${HOME}/.claude" |
| 134 | +mkdir -p "${CLAUDE_CONFIG_DIR}" |
| 135 | +DOCKER_RUN_ARGS+=(-v "${CLAUDE_CONFIG_DIR}:${CONTAINER_HOME}/.claude") |
| 136 | + |
| 137 | +# Claude Code config file (lives next to ~/.claude/, not inside it) |
| 138 | +if [[ -f "${HOME}/.claude.json" ]]; then |
| 139 | + DOCKER_RUN_ARGS+=(-v "${HOME}/.claude.json:${CONTAINER_HOME}/.claude.json") |
| 140 | +fi |
| 141 | + |
| 142 | +# Persist Bazel cache (avoids re-downloading hundreds of MB each run) |
| 143 | +BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR:-${HOME}/.cache/bazel-claude}" |
| 144 | +mkdir -p "${BAZEL_CACHE_DIR}" |
| 145 | +DOCKER_RUN_ARGS+=(-v "${BAZEL_CACHE_DIR}:${CONTAINER_HOME}/.cache/bazel") |
| 146 | + |
| 147 | +# --- Run --- |
| 148 | +if [[ "${SHELL_MODE}" -eq 1 ]]; then |
| 149 | + exec docker run \ |
| 150 | + "${DOCKER_RUN_ARGS[@]}" \ |
| 151 | + "${CLAUDE_IMAGE}" \ |
| 152 | + bash |
| 153 | +else |
| 154 | + exec docker run \ |
| 155 | + "${DOCKER_RUN_ARGS[@]}" \ |
| 156 | + "${CLAUDE_IMAGE}" \ |
| 157 | + claude --dangerously-skip-permissions "${CLAUDE_ARGS[@]}" |
| 158 | +fi |
0 commit comments