Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions registry/coder/modules/claude-code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand All @@ -72,16 +75,17 @@ 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"

claude_api_key = "xxxx-xxxxx-xxxx"
# 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"
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
11 changes: 3 additions & 8 deletions registry/coder/modules/claude-code/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
27 changes: 16 additions & 11 deletions registry/coder/modules/claude-code/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
}

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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}' \
Expand Down Expand Up @@ -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}' \
Expand Down
74 changes: 70 additions & 4 deletions registry/coder/modules/claude-code/scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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"
Expand All @@ -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
}

Expand Down Expand Up @@ -141,5 +206,6 @@ function report_tasks() {
}

install_claude_code_cli
ensure_claude_in_path
setup_claude_configurations
report_tasks
6 changes: 0 additions & 6 deletions registry/coder/modules/claude-code/scripts/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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:-}
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down