diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index c0004504..97f8e46a 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.3.0" + version = "4.4.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -44,8 +44,8 @@ This example shows how to configure the Claude Code module to run the agent behi ```tf module "claude-code" { - source = "dev.registry.coder.com/coder/claude-code/coder" - version = "4.3.0" + source = "registry.coder.com/coder/claude-code/coder" + version = "4.4.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_boundary = true @@ -57,6 +57,9 @@ module "claude-code" { This example shows how to configure the Claude Code module with an AI prompt, API key shared by all users of the template, and other custom settings. +> [!NOTE] +> When a specific `claude_code_version` (other than "latest") is provided, the module will install Claude Code via npm instead of the official installer. This allows for version pinning. The `claude_binary_path` variable can be used to specify where a pre-installed Claude binary is located. + ```tf data "coder_parameter" "ai_prompt" { type = "string" @@ -68,7 +71,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.3.0" + version = "4.4.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -76,7 +79,8 @@ module "claude-code" { # OR claude_code_oauth_token = "xxxxx-xxxx-xxxx" - claude_code_version = "2.0.62" # Pin to a specific version + claude_code_version = "2.0.62" # Pin to a specific version (uses npm) + claude_binary_path = "/opt/claude/bin" # Path to pre-installed Claude binary agentapi_version = "0.11.4" ai_prompt = data.coder_parameter.ai_prompt.value @@ -104,7 +108,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.3.0" + version = "4.4.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -126,7 +130,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.3.0" + version = "4.4.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -199,7 +203,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.3.0" + version = "4.4.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -256,7 +260,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.3.0" + version = "4.4.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index c62493cf..d59b6a8f 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -184,20 +184,15 @@ describe("claude-code", async () => { test("claude-model", async () => { const model = "opus"; - const { id } = await setup({ + const { coderEnvVars } = await setup({ moduleVariables: { model: model, ai_prompt: "test prompt", }, }); - await execModuleScript(id); - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - expect(startLog.stdout).toContain(`--model ${model}`); + // Verify ANTHROPIC_MODEL env var is set via coder_env + expect(coderEnvVars["ANTHROPIC_MODEL"]).toBe(model); }); test("claude-continue-resume-task-session", async () => { diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 62f24c36..192dafdd 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -128,7 +128,7 @@ variable "claude_api_key" { variable "model" { type = string - description = "Sets the model for the current session with an alias for the latest model (sonnet or opus) or a model’s full name." + description = "Sets the default model for Claude Code via ANTHROPIC_MODEL env var. If empty, Claude Code uses its default. Supports aliases (sonnet, opus) or full model names." default = "" } @@ -198,6 +198,18 @@ variable "claude_md_path" { default = "$HOME/.claude/CLAUDE.md" } +variable "claude_binary_path" { + type = string + description = "Directory where the Claude Code binary is located. Use this if Claude is pre-installed or installed outside the module to a non-default location." + default = "$HOME/.local/bin" +} + +variable "install_via_npm" { + type = bool + description = "Install Claude Code via npm instead of the official installer. Useful if npm is preferred or the official installer fails." + default = false +} + variable "enable_boundary" { type = bool description = "Whether to enable coder boundary for network filtering" @@ -217,8 +229,7 @@ variable "compile_boundary_from_source" { } resource "coder_env" "claude_code_md_path" { - count = var.claude_md_path == "" ? 0 : 1 - + count = var.claude_md_path == "" ? 0 : 1 agent_id = var.agent_id name = "CODER_MCP_CLAUDE_MD_PATH" value = var.claude_md_path @@ -237,16 +248,14 @@ resource "coder_env" "claude_code_oauth_token" { } resource "coder_env" "claude_api_key" { - count = length(var.claude_api_key) > 0 ? 1 : 0 - + count = length(var.claude_api_key) > 0 ? 1 : 0 agent_id = var.agent_id name = "CLAUDE_API_KEY" value = var.claude_api_key } resource "coder_env" "disable_autoupdater" { - count = var.disable_autoupdater ? 1 : 0 - + count = var.disable_autoupdater ? 1 : 0 agent_id = var.agent_id name = "DISABLE_AUTOUPDATER" value = "1" @@ -255,7 +264,21 @@ resource "coder_env" "disable_autoupdater" { resource "coder_env" "claude_binary_path" { agent_id = var.agent_id name = "PATH" - value = "$HOME/.local/bin:$PATH" + value = "${var.claude_binary_path}:$PATH" + + lifecycle { + precondition { + condition = var.claude_binary_path == "$HOME/.local/bin" || !var.install_claude_code + error_message = "Custom claude_binary_path can only be used when install_claude_code is false. The official installer and npm both install to fixed locations." + } + } +} + +resource "coder_env" "anthropic_model" { + count = var.model != "" ? 1 : 0 + agent_id = var.agent_id + name = "ANTHROPIC_MODEL" + value = var.model } locals { @@ -328,7 +351,6 @@ module "agentapi" { echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh chmod +x /tmp/start.sh - ARG_MODEL='${var.model}' \ ARG_RESUME_SESSION_ID='${var.resume_session_id}' \ ARG_CONTINUE='${var.continue}' \ ARG_DANGEROUSLY_SKIP_PERMISSIONS='${var.dangerously_skip_permissions}' \ @@ -353,6 +375,8 @@ module "agentapi" { ARG_CLAUDE_CODE_VERSION='${var.claude_code_version}' \ ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \ ARG_INSTALL_CLAUDE_CODE='${var.install_claude_code}' \ + ARG_CLAUDE_BINARY_PATH='${var.claude_binary_path}' \ + ARG_INSTALL_VIA_NPM='${var.install_via_npm}' \ ARG_REPORT_TASKS='${var.report_tasks}' \ ARG_WORKDIR='${local.workdir}' \ ARG_ALLOWED_TOOLS='${var.allowed_tools}' \ diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 15981e8b..47fa651d 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -11,6 +11,8 @@ command_exists() { ARG_CLAUDE_CODE_VERSION=${ARG_CLAUDE_CODE_VERSION:-} ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"} ARG_INSTALL_CLAUDE_CODE=${ARG_INSTALL_CLAUDE_CODE:-} +ARG_CLAUDE_BINARY_PATH=${ARG_CLAUDE_BINARY_PATH:-"$HOME/.local/bin"} +ARG_INSTALL_VIA_NPM=${ARG_INSTALL_VIA_NPM:-false} ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true} ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-} ARG_MCP=$(echo -n "${ARG_MCP:-}" | base64 -d) @@ -22,6 +24,8 @@ echo "--------------------------------" printf "ARG_CLAUDE_CODE_VERSION: %s\n" "$ARG_CLAUDE_CODE_VERSION" printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR" printf "ARG_INSTALL_CLAUDE_CODE: %s\n" "$ARG_INSTALL_CLAUDE_CODE" +printf "ARG_CLAUDE_BINARY_PATH: %s\n" "$ARG_CLAUDE_BINARY_PATH" +printf "ARG_INSTALL_VIA_NPM: %s\n" "$ARG_INSTALL_VIA_NPM" printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS" printf "ARG_MCP_APP_STATUS_SLUG: %s\n" "$ARG_MCP_APP_STATUS_SLUG" printf "ARG_MCP: %s\n" "$ARG_MCP" @@ -30,20 +34,66 @@ printf "ARG_DISALLOWED_TOOLS: %s\n" "$ARG_DISALLOWED_TOOLS" echo "--------------------------------" +function ensure_claude_in_path() { + if [ -z "${CODER_SCRIPT_BIN_DIR:-}" ]; then + echo "CODER_SCRIPT_BIN_DIR not set, skipping PATH setup" + return + fi + + if [ ! -e "$CODER_SCRIPT_BIN_DIR/claude" ]; then + local CLAUDE_BIN="" + if command -v claude > /dev/null 2>&1; then + CLAUDE_BIN=$(command -v claude) + elif [ -x "$ARG_CLAUDE_BINARY_PATH/claude" ]; then + CLAUDE_BIN="$ARG_CLAUDE_BINARY_PATH/claude" + elif [ -x "$HOME/.local/bin/claude" ]; then + CLAUDE_BIN="$HOME/.local/bin/claude" + fi + + if [ -n "$CLAUDE_BIN" ] && [ -x "$CLAUDE_BIN" ]; then + ln -s "$CLAUDE_BIN" "$CODER_SCRIPT_BIN_DIR/claude" + echo "Created symlink: $CODER_SCRIPT_BIN_DIR/claude -> $CLAUDE_BIN" + else + echo "Warning: Could not find claude binary to symlink" + fi + else + echo "Claude already available in CODER_SCRIPT_BIN_DIR" + fi + + local marker="# Added by claude-code module" + for profile in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do + if [ -f "$profile" ] && ! grep -q "$marker" "$profile" 2> /dev/null; then + printf "\n%s\nexport PATH=\"%s:\$PATH\"\n" "$marker" "$CODER_SCRIPT_BIN_DIR" >> "$profile" + echo "Added $CODER_SCRIPT_BIN_DIR to PATH in $profile" + fi + done +} + function install_claude_code_cli() { - if [ "$ARG_INSTALL_CLAUDE_CODE" = "true" ]; then + if [ "$ARG_INSTALL_CLAUDE_CODE" != "true" ]; then + echo "Skipping Claude Code installation as per configuration." + ensure_claude_in_path + return + fi + + # Use npm when install_via_npm is true or for specific version pinning + if [ "$ARG_INSTALL_VIA_NPM" = "true" ] || { [ -n "$ARG_CLAUDE_CODE_VERSION" ] && [ "$ARG_CLAUDE_CODE_VERSION" != "latest" ]; }; then + echo "Installing Claude Code via npm (version: $ARG_CLAUDE_CODE_VERSION)" + npm install -g "@anthropic-ai/claude-code@$ARG_CLAUDE_CODE_VERSION" + echo "Installed Claude Code via npm. Version: $(claude --version || echo 'unknown')" + else echo "Installing Claude Code via official installer" set +e curl -fsSL claude.ai/install.sh | bash -s -- "$ARG_CLAUDE_CODE_VERSION" 2>&1 CURL_EXIT=${PIPESTATUS[0]} set -e if [ $CURL_EXIT -ne 0 ]; then - echo "Claude Code installer failed with exit code $$CURL_EXIT" + echo "Claude Code installer failed with exit code $CURL_EXIT" fi echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')" - else - echo "Skipping Claude Code installation as per configuration." fi + + ensure_claude_in_path } function setup_claude_configurations() { diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index e14b26a6..ad5270b9 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -6,7 +6,6 @@ command_exists() { command -v "$1" > /dev/null 2>&1 } -ARG_MODEL=${ARG_MODEL:-} ARG_RESUME_SESSION_ID=${ARG_RESUME_SESSION_ID:-} ARG_CONTINUE=${ARG_CONTINUE:-false} ARG_DANGEROUSLY_SKIP_PERMISSIONS=${ARG_DANGEROUSLY_SKIP_PERMISSIONS:-} @@ -21,7 +20,6 @@ ARG_CODER_HOST=${ARG_CODER_HOST:-} echo "--------------------------------" -printf "ARG_MODEL: %s\n" "$ARG_MODEL" printf "ARG_RESUME: %s\n" "$ARG_RESUME_SESSION_ID" printf "ARG_CONTINUE: %s\n" "$ARG_CONTINUE" printf "ARG_DANGEROUSLY_SKIP_PERMISSIONS: %s\n" "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" @@ -170,10 +168,6 @@ function start_agentapi() { mkdir -p "$ARG_WORKDIR" cd "$ARG_WORKDIR" - if [ -n "$ARG_MODEL" ]; then - ARGS+=(--model "$ARG_MODEL") - fi - if [ -n "$ARG_PERMISSION_MODE" ]; then ARGS+=(--permission-mode "$ARG_PERMISSION_MODE") fi