diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 11172010f..d66c74fc6 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.2.7" + version = "4.3.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -45,7 +45,7 @@ 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.2.7" + version = "4.3.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_boundary = true @@ -61,6 +61,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") or a custom `claude_binary_path` is provided, the module will install Claude Code via npm instead of the official installer. This allows for version pinning and custom install locations. + ```tf data "coder_parameter" "ai_prompt" { type = "string" @@ -72,7 +75,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.2.7" + version = "4.3.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -80,8 +83,9 @@ module "claude-code" { # OR claude_code_oauth_token = "xxxxx-xxxx-xxxx" - claude_code_version = "2.0.62" # Pin to a specific version - agentapi_version = "0.11.4" + claude_code_version = "2.0.62" # Pin to a specific version + claude_binary_path = "/opt/claude/bin" # Custom install location + agentapi_version = "0.11.4" ai_prompt = data.coder_parameter.ai_prompt.value model = "sonnet" @@ -108,7 +112,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.2.7" + version = "4.3.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -130,7 +134,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.2.7" + version = "4.3.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -203,7 +207,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.2.7" + version = "4.3.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -260,7 +264,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.2.7" + version = "4.3.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 c62493cf8..d59b6a8f4 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 8531a9f9a..c8c1f1007 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,12 @@ variable "claude_md_path" { default = "$HOME/.claude/CLAUDE.md" } +variable "claude_binary_path" { + type = string + description = "Directory where the Claude Code binary is located. If a custom path is specified, Claude Code will be installed via npm to that location instead of using the official installer." + default = "$HOME/.local/bin" +} + variable "enable_boundary" { type = bool description = "Whether to enable coder boundary for network filtering" @@ -253,8 +259,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 @@ -273,25 +278,25 @@ 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" } -resource "coder_env" "claude_binary_path" { + +resource "coder_env" "anthropic_model" { + count = var.model != "" ? 1 : 0 agent_id = var.agent_id - name = "PATH" - value = "$HOME/.local/bin:$PATH" + name = "ANTHROPIC_MODEL" + value = var.model } locals { @@ -364,7 +369,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}' \ @@ -393,6 +397,7 @@ module "agentapi" { echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh chmod +x /tmp/install.sh ARG_CLAUDE_CODE_VERSION='${var.claude_code_version}' \ + ARG_CLAUDE_BINARY_PATH='${var.claude_binary_path}' \ ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \ ARG_INSTALL_CLAUDE_CODE='${var.install_claude_code}' \ ARG_REPORT_TASKS='${var.report_tasks}' \ diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 15981e8b5..2dc0c46ff 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -9,6 +9,7 @@ command_exists() { } ARG_CLAUDE_CODE_VERSION=${ARG_CLAUDE_CODE_VERSION:-} +ARG_CLAUDE_BINARY_PATH=${ARG_CLAUDE_BINARY_PATH:-'$HOME/.local/bin'} ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"} ARG_INSTALL_CLAUDE_CODE=${ARG_INSTALL_CLAUDE_CODE:-} ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true} @@ -17,9 +18,13 @@ ARG_MCP=$(echo -n "${ARG_MCP:-}" | base64 -d) ARG_ALLOWED_TOOLS=${ARG_ALLOWED_TOOLS:-} ARG_DISALLOWED_TOOLS=${ARG_DISALLOWED_TOOLS:-} +ARG_CLAUDE_BINARY_PATH=$(eval echo "$ARG_CLAUDE_BINARY_PATH") +DEFAULT_BINARY_PATH="$HOME/.local/bin" + echo "--------------------------------" printf "ARG_CLAUDE_CODE_VERSION: %s\n" "$ARG_CLAUDE_CODE_VERSION" +printf "ARG_CLAUDE_BINARY_PATH: %s\n" "$ARG_CLAUDE_BINARY_PATH" printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR" printf "ARG_INSTALL_CLAUDE_CODE: %s\n" "$ARG_INSTALL_CLAUDE_CODE" printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS" @@ -30,19 +35,79 @@ printf "ARG_DISALLOWED_TOOLS: %s\n" "$ARG_DISALLOWED_TOOLS" echo "--------------------------------" +# Ensures claude is accessible in PATH when using a custom binary path +# Creates symlink in ~/.local/bin and adds to shell profiles +function ensure_claude_in_path() { + if [ "$ARG_CLAUDE_BINARY_PATH" = "$DEFAULT_BINARY_PATH" ]; then + # Default path - no action needed, official installer handles this + return + fi + + echo "Setting up PATH for custom claude location: $ARG_CLAUDE_BINARY_PATH" + + # Create symlink in ~/.local/bin so claude is accessible in PATH + mkdir -p "$HOME/.local/bin" + ln -sf "$ARG_CLAUDE_BINARY_PATH/claude" "$HOME/.local/bin/claude" + echo "Created symlink: $HOME/.local/bin/claude -> $ARG_CLAUDE_BINARY_PATH/claude" + + # Ensure ~/.local/bin is in PATH for this session (needed for claude mcp commands below) + export PATH="$HOME/.local/bin:$PATH" + + # Add to shell profiles for future interactive sessions + # Only modifies files that already exist, uses marker to prevent duplicates + local marker="# Added by claude-code module" + local path_export='export PATH="$HOME/.local/bin:$PATH"' + + for profile in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do + if [ -f "$profile" ] && ! grep -qF "$marker" "$profile" 2>/dev/null; then + echo "" >> "$profile" + echo "$marker" >> "$profile" + echo "$path_export" >> "$profile" + echo "Added ~/.local/bin 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." + return + fi + + local use_npm=false + local specific_version=false + + if [ "$ARG_CLAUDE_BINARY_PATH" != "$DEFAULT_BINARY_PATH" ]; then + use_npm=true + fi + + if [ -n "$ARG_CLAUDE_CODE_VERSION" ] && [ "$ARG_CLAUDE_CODE_VERSION" != "latest" ]; then + use_npm=true + specific_version=true + fi + + if [ "$use_npm" = "true" ]; then + echo "Installing Claude Code via npm (custom path or version specified)" + NPM_PREFIX=$(dirname "$ARG_CLAUDE_BINARY_PATH") + mkdir -p "$NPM_PREFIX" + + local version_arg="" + if [ "$specific_version" = "true" ]; then + version_arg="@$ARG_CLAUDE_CODE_VERSION" + fi + + npm install -g "@anthropic-ai/claude-code${version_arg}" --prefix "$NPM_PREFIX" + echo "Installed Claude Code via npm to $NPM_PREFIX. Version: $($ARG_CLAUDE_BINARY_PATH/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 } @@ -141,5 +206,6 @@ function report_tasks() { } install_claude_code_cli +ensure_claude_in_path setup_claude_configurations report_tasks diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index ebca736cc..56c9eff62 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:-} @@ -26,7 +25,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" @@ -171,10 +169,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