diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 3d875046f..6c25c8cc0 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.8.0" + version = "4.8.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -60,7 +60,7 @@ By default, when `enable_boundary = true`, the module uses `coder boundary` subc ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.8.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_boundary = true @@ -81,7 +81,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.8.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_aibridge = true @@ -110,7 +110,7 @@ data "coder_task" "me" {} module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.8.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" ai_prompt = data.coder_task.me.prompt @@ -133,7 +133,7 @@ This example shows additional configuration options for version pinning, custom ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.8.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -189,7 +189,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.8.0" + version = "4.8.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -211,7 +211,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.8.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -284,7 +284,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.8.1" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -341,7 +341,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.8.1" 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 19ab98c04..1f3904a32 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -415,6 +415,58 @@ EOF`, expect(startLog.stdout).toContain("--continue"); }); + test("standalone-oauth-only-does-not-create-empty-primary-api-key", async () => { + const { id, coderEnvVars } = await setup({ + moduleVariables: { + report_tasks: "false", + claude_code_oauth_token: "oauth-token-123", + }, + }); + + await execModuleScript(id, coderEnvVars); + + const claudeConfig = JSON.parse( + await readFileContainer(id, "/home/coder/.claude.json"), + ); + + expect(claudeConfig.primaryApiKey).toBeUndefined(); + expect(claudeConfig.hasCompletedOnboarding).toBe(true); + expect( + claudeConfig.projects["/home/coder/project"] + .hasCompletedProjectOnboarding, + ).toBe(true); + }); + + test("standalone-oauth-only-preserves-existing-primary-api-key", async () => { + const existingApiKey = "existing-api-key"; + const { id, coderEnvVars } = await setup({ + moduleVariables: { + report_tasks: "false", + claude_code_oauth_token: "oauth-token-123", + }, + }); + + await execContainer(id, [ + "bash", + "-c", + `cat > /home/coder/.claude.json << 'EOF' +{"primaryApiKey":"${existingApiKey}","projects":{}} +EOF`, + ]); + + await execModuleScript(id, coderEnvVars); + + const claudeConfig = JSON.parse( + await readFileContainer(id, "/home/coder/.claude.json"), + ); + + expect(claudeConfig.primaryApiKey).toBe(existingApiKey); + expect(claudeConfig.hasCompletedOnboarding).toBe(true); + expect( + claudeConfig.projects["/home/coder/project"].hasTrustDialogAccepted, + ).toBe(true); + }); + test("task-mode-ignores-manual-sessions", async () => { const { id } = await setup({ moduleVariables: { diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 0a2ba7033..da02691a9 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -179,14 +179,14 @@ function setup_claude_configurations() { function configure_standalone_mode() { echo "Configuring Claude Code for standalone mode..." - if [ -z "${CLAUDE_API_KEY:-}" ] && [ "$ARG_ENABLE_AIBRIDGE" = "false" ]; then - echo "Note: Neither claude_api_key nor enable_aibridge is set, skipping authentication setup" + if [ -z "${CLAUDE_API_KEY:-}" ] \ + && [ -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ] \ + && [ "${ARG_ENABLE_AIBRIDGE:-false}" = "false" ]; then + echo "Note: No CLAUDE_API_KEY, no CLAUDE_CODE_OAUTH_TOKEN, and AIBridge disabled — skipping authentication setup" return fi local claude_config="$HOME/.claude.json" - local workdir_normalized - workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') # Create or update .claude.json with minimal configuration for API key auth # This skips the interactive login prompt and onboarding screens @@ -198,27 +198,27 @@ function configure_standalone_mode() { .bypassPermissionsModeAccepted = true | .hasAcknowledgedCostThreshold = true | .hasCompletedOnboarding = true | - .primaryApiKey = $apikey | .projects[$workdir].hasCompletedProjectOnboarding = true | - .projects[$workdir].hasTrustDialogAccepted = true' \ + .projects[$workdir].hasTrustDialogAccepted = true | + if $apikey != "" then .primaryApiKey = $apikey else . end' \ "$claude_config" > "${claude_config}.tmp" && mv "${claude_config}.tmp" "$claude_config" else echo "Creating new Claude configuration at $claude_config" - cat > "$claude_config" << EOF -{ - "autoUpdaterStatus": "disabled", - "bypassPermissionsModeAccepted": true, - "hasAcknowledgedCostThreshold": true, - "hasCompletedOnboarding": true, - "primaryApiKey": "${CLAUDE_API_KEY:-}", - "projects": { - "$ARG_WORKDIR": { - "hasCompletedProjectOnboarding": true, - "hasTrustDialogAccepted": true - } - } -} -EOF + jq -n --arg workdir "$ARG_WORKDIR" --arg apikey "${CLAUDE_API_KEY:-}" \ + '{ + autoUpdaterStatus: "disabled", + bypassPermissionsModeAccepted: true, + hasAcknowledgedCostThreshold: true, + hasCompletedOnboarding: true, + projects: { + ($workdir): { + hasCompletedProjectOnboarding: true, + hasTrustDialogAccepted: true + } + } + } | + if $apikey != "" then . + {primaryApiKey: $apikey} else . end' \ + > "$claude_config" fi echo "Standalone mode configured successfully"