chore: add input for path, and use coder_env for model in claude-code module (#617)

## Description

Adds a `claude_path` variable to override the path that the `coder_env`
sets for claude-code.
Adds a `install_via_npm` variable to override the official installation
and use npm to install claude-code ( this doesn't change any behaviour,
simply just the options available to the user )

Model now uses `coder_env` to set the `ANTHROPIC_MODEL` env instead of
using the `--model` cli flag which did not set the anthropic model
globally in the workspace.

Normal Usage -> Official Installer
Specific Version Set -> NPM Install
Install Via NPM -> NPM Install

The idea with `claude_path` is that someone who wants to source claude
another way can install it and utilize that install in the module. If
install is true and a custom path is provided there is a tf precondition
that will disallow this.

<!-- Briefly describe what this PR does and why -->

## Type of Change

- [ ] New module
- [ ] New template
- [ ] Bug fix
- [X] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/claude-code`  
**New version:** `v4.4.0`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun fmt`)
- [X] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
This commit is contained in:
DevCats
2026-01-13 14:12:10 -06:00
committed by GitHub
parent 2a40c07c3a
commit 44a8c66d94
5 changed files with 103 additions and 36 deletions
+13 -9
View File
@@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
```tf ```tf
module "claude-code" { module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder" source = "registry.coder.com/coder/claude-code/coder"
version = "4.3.0" version = "4.4.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
workdir = "/home/coder/project" workdir = "/home/coder/project"
claude_api_key = "xxxx-xxxxx-xxxx" 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 ```tf
module "claude-code" { module "claude-code" {
source = "dev.registry.coder.com/coder/claude-code/coder" source = "registry.coder.com/coder/claude-code/coder"
version = "4.3.0" version = "4.4.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
workdir = "/home/coder/project" workdir = "/home/coder/project"
enable_boundary = true 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. 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 ```tf
data "coder_parameter" "ai_prompt" { data "coder_parameter" "ai_prompt" {
type = "string" type = "string"
@@ -68,7 +71,7 @@ data "coder_parameter" "ai_prompt" {
module "claude-code" { module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder" source = "registry.coder.com/coder/claude-code/coder"
version = "4.3.0" version = "4.4.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
workdir = "/home/coder/project" workdir = "/home/coder/project"
@@ -76,7 +79,8 @@ module "claude-code" {
# OR # OR
claude_code_oauth_token = "xxxxx-xxxx-xxxx" 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" agentapi_version = "0.11.4"
ai_prompt = data.coder_parameter.ai_prompt.value 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 ```tf
module "claude-code" { module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder" source = "registry.coder.com/coder/claude-code/coder"
version = "4.3.0" version = "4.4.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
workdir = "/home/coder/project" workdir = "/home/coder/project"
install_claude_code = true install_claude_code = true
@@ -126,7 +130,7 @@ variable "claude_code_oauth_token" {
module "claude-code" { module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder" source = "registry.coder.com/coder/claude-code/coder"
version = "4.3.0" version = "4.4.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
workdir = "/home/coder/project" workdir = "/home/coder/project"
claude_code_oauth_token = var.claude_code_oauth_token claude_code_oauth_token = var.claude_code_oauth_token
@@ -199,7 +203,7 @@ resource "coder_env" "bedrock_api_key" {
module "claude-code" { module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder" source = "registry.coder.com/coder/claude-code/coder"
version = "4.3.0" version = "4.4.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
workdir = "/home/coder/project" workdir = "/home/coder/project"
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
@@ -256,7 +260,7 @@ resource "coder_env" "google_application_credentials" {
module "claude-code" { module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder" source = "registry.coder.com/coder/claude-code/coder"
version = "4.3.0" version = "4.4.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
workdir = "/home/coder/project" workdir = "/home/coder/project"
model = "claude-sonnet-4@20250514" model = "claude-sonnet-4@20250514"
@@ -184,20 +184,15 @@ describe("claude-code", async () => {
test("claude-model", async () => { test("claude-model", async () => {
const model = "opus"; const model = "opus";
const { id } = await setup({ const { coderEnvVars } = await setup({
moduleVariables: { moduleVariables: {
model: model, model: model,
ai_prompt: "test prompt", ai_prompt: "test prompt",
}, },
}); });
await execModuleScript(id);
const startLog = await execContainer(id, [ // Verify ANTHROPIC_MODEL env var is set via coder_env
"bash", expect(coderEnvVars["ANTHROPIC_MODEL"]).toBe(model);
"-c",
"cat /home/coder/.claude-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain(`--model ${model}`);
}); });
test("claude-continue-resume-task-session", async () => { test("claude-continue-resume-task-session", async () => {
+33 -9
View File
@@ -128,7 +128,7 @@ variable "claude_api_key" {
variable "model" { variable "model" {
type = string type = string
description = "Sets the model for the current session with an alias for the latest model (sonnet or opus) or a models 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 = "" default = ""
} }
@@ -198,6 +198,18 @@ variable "claude_md_path" {
default = "$HOME/.claude/CLAUDE.md" 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" { variable "enable_boundary" {
type = bool type = bool
description = "Whether to enable coder boundary for network filtering" 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" { 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 agent_id = var.agent_id
name = "CODER_MCP_CLAUDE_MD_PATH" name = "CODER_MCP_CLAUDE_MD_PATH"
value = var.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" { 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 agent_id = var.agent_id
name = "CLAUDE_API_KEY" name = "CLAUDE_API_KEY"
value = var.claude_api_key value = var.claude_api_key
} }
resource "coder_env" "disable_autoupdater" { resource "coder_env" "disable_autoupdater" {
count = var.disable_autoupdater ? 1 : 0 count = var.disable_autoupdater ? 1 : 0
agent_id = var.agent_id agent_id = var.agent_id
name = "DISABLE_AUTOUPDATER" name = "DISABLE_AUTOUPDATER"
value = "1" value = "1"
@@ -255,7 +264,21 @@ resource "coder_env" "disable_autoupdater" {
resource "coder_env" "claude_binary_path" { resource "coder_env" "claude_binary_path" {
agent_id = var.agent_id agent_id = var.agent_id
name = "PATH" 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 { locals {
@@ -328,7 +351,6 @@ module "agentapi" {
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh chmod +x /tmp/start.sh
ARG_MODEL='${var.model}' \
ARG_RESUME_SESSION_ID='${var.resume_session_id}' \ ARG_RESUME_SESSION_ID='${var.resume_session_id}' \
ARG_CONTINUE='${var.continue}' \ ARG_CONTINUE='${var.continue}' \
ARG_DANGEROUSLY_SKIP_PERMISSIONS='${var.dangerously_skip_permissions}' \ ARG_DANGEROUSLY_SKIP_PERMISSIONS='${var.dangerously_skip_permissions}' \
@@ -353,6 +375,8 @@ module "agentapi" {
ARG_CLAUDE_CODE_VERSION='${var.claude_code_version}' \ ARG_CLAUDE_CODE_VERSION='${var.claude_code_version}' \
ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \ ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
ARG_INSTALL_CLAUDE_CODE='${var.install_claude_code}' \ 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_REPORT_TASKS='${var.report_tasks}' \
ARG_WORKDIR='${local.workdir}' \ ARG_WORKDIR='${local.workdir}' \
ARG_ALLOWED_TOOLS='${var.allowed_tools}' \ ARG_ALLOWED_TOOLS='${var.allowed_tools}' \
@@ -11,6 +11,8 @@ command_exists() {
ARG_CLAUDE_CODE_VERSION=${ARG_CLAUDE_CODE_VERSION:-} ARG_CLAUDE_CODE_VERSION=${ARG_CLAUDE_CODE_VERSION:-}
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"} ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
ARG_INSTALL_CLAUDE_CODE=${ARG_INSTALL_CLAUDE_CODE:-} 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_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-} ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
ARG_MCP=$(echo -n "${ARG_MCP:-}" | base64 -d) 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_CLAUDE_CODE_VERSION: %s\n" "$ARG_CLAUDE_CODE_VERSION"
printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR" printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR"
printf "ARG_INSTALL_CLAUDE_CODE: %s\n" "$ARG_INSTALL_CLAUDE_CODE" 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_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
printf "ARG_MCP_APP_STATUS_SLUG: %s\n" "$ARG_MCP_APP_STATUS_SLUG" printf "ARG_MCP_APP_STATUS_SLUG: %s\n" "$ARG_MCP_APP_STATUS_SLUG"
printf "ARG_MCP: %s\n" "$ARG_MCP" printf "ARG_MCP: %s\n" "$ARG_MCP"
@@ -30,20 +34,66 @@ printf "ARG_DISALLOWED_TOOLS: %s\n" "$ARG_DISALLOWED_TOOLS"
echo "--------------------------------" 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() { 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" echo "Installing Claude Code via official installer"
set +e set +e
curl -fsSL claude.ai/install.sh | bash -s -- "$ARG_CLAUDE_CODE_VERSION" 2>&1 curl -fsSL claude.ai/install.sh | bash -s -- "$ARG_CLAUDE_CODE_VERSION" 2>&1
CURL_EXIT=${PIPESTATUS[0]} CURL_EXIT=${PIPESTATUS[0]}
set -e set -e
if [ $CURL_EXIT -ne 0 ]; then 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 fi
echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')" echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')"
else
echo "Skipping Claude Code installation as per configuration."
fi fi
ensure_claude_in_path
} }
function setup_claude_configurations() { function setup_claude_configurations() {
@@ -6,7 +6,6 @@ command_exists() {
command -v "$1" > /dev/null 2>&1 command -v "$1" > /dev/null 2>&1
} }
ARG_MODEL=${ARG_MODEL:-}
ARG_RESUME_SESSION_ID=${ARG_RESUME_SESSION_ID:-} ARG_RESUME_SESSION_ID=${ARG_RESUME_SESSION_ID:-}
ARG_CONTINUE=${ARG_CONTINUE:-false} ARG_CONTINUE=${ARG_CONTINUE:-false}
ARG_DANGEROUSLY_SKIP_PERMISSIONS=${ARG_DANGEROUSLY_SKIP_PERMISSIONS:-} ARG_DANGEROUSLY_SKIP_PERMISSIONS=${ARG_DANGEROUSLY_SKIP_PERMISSIONS:-}
@@ -21,7 +20,6 @@ ARG_CODER_HOST=${ARG_CODER_HOST:-}
echo "--------------------------------" echo "--------------------------------"
printf "ARG_MODEL: %s\n" "$ARG_MODEL"
printf "ARG_RESUME: %s\n" "$ARG_RESUME_SESSION_ID" printf "ARG_RESUME: %s\n" "$ARG_RESUME_SESSION_ID"
printf "ARG_CONTINUE: %s\n" "$ARG_CONTINUE" printf "ARG_CONTINUE: %s\n" "$ARG_CONTINUE"
printf "ARG_DANGEROUSLY_SKIP_PERMISSIONS: %s\n" "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" printf "ARG_DANGEROUSLY_SKIP_PERMISSIONS: %s\n" "$ARG_DANGEROUSLY_SKIP_PERMISSIONS"
@@ -170,10 +168,6 @@ function start_agentapi() {
mkdir -p "$ARG_WORKDIR" mkdir -p "$ARG_WORKDIR"
cd "$ARG_WORKDIR" cd "$ARG_WORKDIR"
if [ -n "$ARG_MODEL" ]; then
ARGS+=(--model "$ARG_MODEL")
fi
if [ -n "$ARG_PERMISSION_MODE" ]; then if [ -n "$ARG_PERMISSION_MODE" ]; then
ARGS+=(--permission-mode "$ARG_PERMISSION_MODE") ARGS+=(--permission-mode "$ARG_PERMISSION_MODE")
fi fi