feat(docker): remove Claude Code CLI from default image; template fails fast

The Claude Code CLI (~245MB, the single largest optional component) was
bundled unconditionally and is used only by the claude sandbox template.
Gate it behind WITH_CLAUDE_CODE (default false); opt back in with
--build-arg WITH_CLAUDE_CODE=true or install it at worker startup via an
init script.

To keep the failure actionable, the claude sandbox template now checks for
the CLI at /usr/bin/claude before invoking the agent SDK and, when absent,
throws a clear error naming the next step: the exact init-script install
command plus a docs link. The hardcoded executable path is lifted into a
CLAUDE_CODE_EXECUTABLE constant referenced by both the check and the SDK
call so they can't drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-07-08 09:52:31 +00:00
parent b4e2dd4aa9
commit 28f382b317
2 changed files with 29 additions and 6 deletions
+13 -5
View File
@@ -145,6 +145,9 @@ ARG WITH_POWERSHELL=true
ARG WITH_KUBECTL=true
# helm is no longer bundled in the default image; opt back in with WITH_HELM=true
ARG WITH_HELM=false
# the Claude Code CLI (~245MB, used only by the claude sandbox template) is no longer
# bundled in the default image; opt back in with WITH_CLAUDE_CODE=true
ARG WITH_CLAUDE_CODE=false
ARG WITH_GIT=true
ARG features=""
@@ -299,11 +302,16 @@ COPY --from=oven/bun:1.3.10 /usr/local/bin/bun /usr/bin/bun
RUN bun install -g windmill-cli \
&& ln -s $(bun pm bin -g)/wmill /usr/bin/wmill
# Install Claude Code CLI (used by claude sandbox scripts)
# The installer puts the binary in ~/.local/bin/claude (symlink to ~/.local/share/claude/versions/*)
# Copy it to /usr/bin/claude so it's accessible inside nsjail sandbox (which mounts /usr but not /root)
RUN curl -fsSL https://claude.ai/install.sh | bash \
&& cp /root/.local/share/claude/versions/* /usr/bin/claude
# Claude Code CLI (used only by the claude sandbox script template). Not bundled by
# default to keep the image lean (~245MB); opt in with --build-arg WITH_CLAUDE_CODE=true,
# or install it at worker startup via an init script. When absent, the claude sandbox
# template fails fast with instructions (frontend/src/lib/templates/claude_sandbox.ts.template).
# The installer puts the binary in ~/.local/bin/claude (symlink to ~/.local/share/claude/versions/*);
# copy it to /usr/bin/claude so it's reachable inside the nsjail sandbox (which mounts /usr but not /root).
RUN if [ "$WITH_CLAUDE_CODE" = "true" ]; then \
curl -fsSL https://claude.ai/install.sh | bash \
&& cp /root/.local/share/claude/versions/* /usr/bin/claude; \
else echo 'Building the image without the Claude Code CLI'; fi
COPY --from=php:8.3.30-cli-bookworm /usr/local/bin/php /usr/bin/php
COPY --from=composer:2.9.5 /usr/bin/composer /usr/bin/composer
@@ -8,8 +8,23 @@ import * as path from "path";
// path -> content, fileset resource, that contains your CLAUDE.md, and skills
type AgentInstructions = Record<string, string>
// The Claude Code CLI is not bundled in the default Windmill image. Install it on the
// worker via an init script (Worker Management > Init scripts) with:
// curl -fsSL https://claude.ai/install.sh | bash && cp /root/.local/share/claude/versions/* /usr/bin/claude
// See https://www.windmill.dev/docs/core_concepts/worker_groups#init-scripts
const CLAUDE_CODE_EXECUTABLE = "/usr/bin/claude";
export async function main(anthropic: RT.Anthropic, agent_instructions?: AgentInstructions) {
if (!fs.existsSync(CLAUDE_CODE_EXECUTABLE)) {
throw new Error(
`Claude Code CLI not found at ${CLAUDE_CODE_EXECUTABLE}. It is not bundled in the default Windmill image. ` +
`Install it on this worker with an init script (Worker Management > Init scripts):\n` +
` curl -fsSL https://claude.ai/install.sh | bash && cp /root/.local/share/claude/versions/* /usr/bin/claude\n` +
`See https://www.windmill.dev/docs/core_concepts/worker_groups#init-scripts`
);
}
const sessionFile = path.join(".claude/session-id.txt");
let sessionId = fs.existsSync(sessionFile) ? fs.readFileSync(sessionFile, "utf-8").trim() : undefined
@@ -40,7 +55,7 @@ export async function main(anthropic: RT.Anthropic, agent_instructions?: AgentIn
prompt,
options: {
model: "opus",
pathToClaudeCodeExecutable: "/usr/bin/claude",
pathToClaudeCodeExecutable: CLAUDE_CODE_EXECUTABLE,
permissionMode: 'bypassPermissions',
allowDangerouslySkipPermissions: true,
...(isResume ? { resume: sessionId } : {}),