From 5112588ca0df5d28c4570ec7885d2af1cb6d50b3 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 24 Apr 2026 07:49:30 +0000 Subject: [PATCH 1/4] chore(registry/coder/modules/claude-code): strip boundary, agentapi, tasks, tools - Remove boundary variables and install logic. - Drop agentapi module; orchestrate scripts through coder-utils instead. - Remove start-script-only variables not shared with install (resume_session_id, continue, ai_prompt, dangerously_skip_permissions). - Remove allowed_tools and disallowed_tools. - Remove report_tasks, system_prompt, claude_md_path, and coder exp mcp / coder_report_task wiring. - Rename claude_api_key to anthropic_api_key (ANTHROPIC_API_KEY). - Rename enable_aibridge to enable_ai_gateway. - Drop install_via_npm; always use the official claude.ai/install.sh installer. - Tighten workdir description and drop dead module_dir_name/task_app_id. - Move module logs to $HOME/.coder-modules/coder/claude-code/logs (matches coder-utils 1.3.0 nested layout). --- registry/coder/modules/claude-code/README.md | 232 +++---- .../coder/modules/claude-code/main.test.ts | 638 ++++++++---------- registry/coder/modules/claude-code/main.tf | 320 ++------- .../coder/modules/claude-code/main.tftest.hcl | 336 ++------- .../modules/claude-code/scripts/install.sh | 94 +-- .../modules/claude-code/scripts/start.sh | 256 ------- .../claude-code/testdata/claude-mock.sh | 9 +- 7 files changed, 498 insertions(+), 1387 deletions(-) delete mode 100644 registry/coder/modules/claude-code/scripts/start.sh diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 48b291bb0..0a082df15 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -1,152 +1,115 @@ --- display_name: Claude Code -description: Run the Claude Code agent in your workspace. +description: Install and configure the Claude Code CLI in your workspace. icon: ../../../../.icons/claude.svg verified: true -tags: [agent, claude-code, ai, tasks, anthropic, aibridge] +tags: [agent, claude-code, ai, anthropic, ai-gateway] --- # Claude Code -Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) agent in your workspace to generate code and perform tasks. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for task reporting in the Coder UI. +Install and configure the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) CLI in your workspace. Starting Claude is left to the caller (template command, IDE launcher, or a custom `coder_script`). ```tf module "claude-code" { - source = "registry.coder.com/coder/claude-code/coder" - version = "4.9.2" - agent_id = coder_agent.main.id - workdir = "/home/coder/project" - claude_api_key = "xxxx-xxxxx-xxxx" + source = "registry.coder.com/coder/claude-code/coder" + version = "5.0.0" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + anthropic_api_key = "xxxx-xxxxx-xxxx" } ``` -> [!WARNING] -> **Security Notice**: This module uses the `--dangerously-skip-permissions` flag when running Claude Code tasks. This flag bypasses standard permission checks and allows Claude Code broader access to your system than normally permitted. While this enables more functionality, it also means Claude Code can potentially execute commands with the same privileges as the user running it. Use this module _only_ in trusted environments and be aware of the security implications. - -> [!NOTE] -> By default, this module is configured to run the embedded chat interface as a path-based application. In production, we recommend that you configure a [wildcard access URL](https://coder.com/docs/admin/setup#wildcard-access-url) and set `subdomain = true`. See [here](https://coder.com/docs/tutorials/best-practices/security-best-practices#disable-path-based-apps) for more details. - ## Prerequisites -- An **Anthropic API key** or a _Claude Session Token_ is required for tasks. - - You can get the API key from the [Anthropic Console](https://console.anthropic.com/dashboard). - - You can get the Session Token using the `claude setup-token` command. This is a long-lived authentication token (requires Claude subscription) +Provide exactly one authentication method: -### Session Resumption Behavior +- **Anthropic API key**: get one from the [Anthropic Console](https://console.anthropic.com/dashboard) and pass it as `anthropic_api_key`. +- **Claude.ai OAuth token** (Pro, Max, or Enterprise accounts): generate one by running `claude setup-token` locally and pass it as `claude_code_oauth_token`. +- **Coder AI Gateway** (Coder Premium, Coder >= 2.30.0): set `enable_ai_gateway = true`. The module authenticates against the gateway using the workspace owner's session token. Do not combine with `anthropic_api_key` or `claude_code_oauth_token`. -By default, Claude Code automatically resumes existing conversations when your workspace restarts. Sessions are tracked per workspace directory, so conversations continue where you left off. If no session exists (first start), your `ai_prompt` will run normally. To disable this behavior and always start fresh, set `continue = false` - -## State Persistence +## Examples -AgentAPI can save and restore its conversation state to disk across workspace restarts. This complements `continue` (which resumes the Claude CLI session) by also preserving the AgentAPI-level context. Enabled by default, requires agentapi >= v0.12.0 (older versions skip it with a warning). +### Standalone mode with a launcher app -To disable: +Authenticate Claude directly against Anthropic's API and add a `coder_app` that users can click from the workspace dashboard to open an interactive Claude session. ```tf -module "claude-code" { - # ... other config - enable_state_persistence = false +locals { + claude_workdir = "/home/coder/project" } -``` - -## Examples - -### Usage with Agent Boundaries - -This example shows how to configure the Claude Code module to run the agent behind a process-level boundary that restricts its network access. - -By default, when `enable_boundary = true`, the module uses `coder boundary` subcommand (provided by Coder) without requiring any installation. -```tf module "claude-code" { - source = "registry.coder.com/coder/claude-code/coder" - version = "4.9.2" - agent_id = coder_agent.main.id - workdir = "/home/coder/project" - enable_boundary = true + source = "registry.coder.com/coder/claude-code/coder" + version = "5.0.0" + agent_id = coder_agent.main.id + workdir = local.claude_workdir + anthropic_api_key = "xxxx-xxxxx-xxxx" +} + +resource "coder_app" "claude" { + agent_id = coder_agent.main.id + slug = "claude" + display_name = "Claude Code" + icon = "/icon/claude.svg" + open_in = "slim-window" + command = <<-EOT + #!/bin/bash + set -e + cd ${local.claude_workdir} + claude + EOT } ``` > [!NOTE] -> For developers: The module also supports installing boundary from a release version (`use_boundary_directly = true`) or compiling from source (`compile_boundary_from_source = true`). These are escape hatches for development and testing purposes. - -### Usage with AI Bridge +> `coder_app.command` runs when the user clicks the app tile. Combine with `anthropic_api_key`, `claude_code_oauth_token`, or `enable_ai_gateway = true` on the module to pre-authenticate the CLI. -[AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) is a Premium Coder feature that provides centralized LLM proxy management. To use AI Bridge, set `enable_aibridge = true`. Requires Coder version >= 2.29.0. +### Usage with AI Gateway -For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage with Tasks](#usage-with-tasks) example below. - -#### Standalone usage with AI Bridge +[AI Gateway](https://coder.com/docs/ai-coder/ai-gateway) is a Premium Coder feature that provides centralized LLM proxy management. Requires Coder >= 2.30.0. ```tf module "claude-code" { - source = "registry.coder.com/coder/claude-code/coder" - version = "4.9.2" - agent_id = coder_agent.main.id - workdir = "/home/coder/project" - enable_aibridge = true + source = "registry.coder.com/coder/claude-code/coder" + version = "5.0.0" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + enable_ai_gateway = true } ``` -When `enable_aibridge = true`, the module automatically sets: +When `enable_ai_gateway = true`, the module sets: - `ANTHROPIC_BASE_URL` to `${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic` -- `CLAUDE_API_KEY` to the workspace owner's session token - -This allows Claude Code to route API requests through Coder's AI Bridge instead of directly to Anthropic's API. -Template build will fail if either `claude_api_key` or `claude_code_oauth_token` is provided alongside `enable_aibridge = true`. - -### Usage with Tasks - -This example shows how to configure Claude Code with Coder tasks. - -```tf -resource "coder_ai_task" "task" { - count = data.coder_workspace.me.start_count - app_id = module.claude-code.task_app_id -} +- `ANTHROPIC_AUTH_TOKEN` to the workspace owner's Coder session token -data "coder_task" "me" {} +Claude Code then routes API requests through Coder's AI Gateway instead of directly to Anthropic. -module "claude-code" { - source = "registry.coder.com/coder/claude-code/coder" - version = "4.9.2" - agent_id = coder_agent.main.id - workdir = "/home/coder/project" - ai_prompt = data.coder_task.me.prompt - - # Optional: route through AI Bridge (Premium feature) - # enable_aibridge = true -} -``` +> [!CAUTION] +> `enable_ai_gateway = true` is mutually exclusive with `anthropic_api_key` and `claude_code_oauth_token`. Setting any of them together fails at plan time. ### Advanced Configuration -This example shows additional configuration options for version pinning, custom models, and MCP servers. - -> [!NOTE] -> The `claude_binary_path` variable can be used to specify where a pre-installed Claude binary is located. - -> [!WARNING] -> **Deprecation Notice**: The npm installation method (`install_via_npm = true`) will be deprecated and removed in the next major release. Please use the default binary installation method instead. +This example shows version pinning, a pre-installed binary path, a custom model, and MCP servers. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.9.2" + version = "5.0.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" + anthropic_api_key = "xxxx-xxxxx-xxxx" + + claude_code_version = "2.0.62" # Pin to a specific Claude CLI version. - claude_code_version = "2.0.62" # Pin to a specific version - claude_binary_path = "/opt/claude/bin" # Path to pre-installed Claude binary - agentapi_version = "0.11.4" + # Skip the module's installer and point at a pre-installed Claude binary. + # claude_binary_path can only be customized when install_claude_code is false. + install_claude_code = false + claude_binary_path = "/opt/claude/bin" - model = "sonnet" - permission_mode = "plan" + model = "sonnet" mcp = <<-EOF { @@ -166,6 +129,9 @@ module "claude-code" { } ``` +> [!NOTE] +> Swap `anthropic_api_key` for `claude_code_oauth_token = "xxxxx-xxxx-xxxx"` to authenticate via a Claude.ai OAuth token instead. Pass exactly one. + > [!NOTE] > Remote URLs should return a JSON body in the following format: > @@ -180,41 +146,37 @@ module "claude-code" { > } > ``` > -> The `Content-Type` header doesn't matter—both `text/plain` and `application/json` work fine. +> The `Content-Type` header doesn't matter, both `text/plain` and `application/json` work fine. + +### Serialize a downstream `coder_script` after the install pipeline -### Standalone Mode +The module exposes the `coder exp sync` name of each script it creates via the `scripts` output: an ordered list (`pre_install`, `install`, `post_install`) of names for scripts this module actually creates. Scripts that were not configured are absent from the list. -Run and configure Claude Code as a standalone CLI in your workspace. +Downstream `coder_script` resources can wait for this module's install pipeline to finish using `coder exp sync want `: ```tf module "claude-code" { - source = "registry.coder.com/coder/claude-code/coder" - version = "4.9.2" - agent_id = coder_agent.main.id - workdir = "/home/coder/project" - install_claude_code = true - claude_code_version = "2.0.62" - report_tasks = false + source = "registry.coder.com/coder/claude-code/coder" + version = "5.0.0" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + anthropic_api_key = "xxxx-xxxxx-xxxx" } -``` -### Usage with Claude Code Subscription - -```tf - -variable "claude_code_oauth_token" { - type = string - description = "Generate one using `claude setup-token` command" - sensitive = true - value = "xxxx-xxx-xxxx" -} +resource "coder_script" "post_claude" { + agent_id = coder_agent.main.id + display_name = "Run after Claude Code install" + run_on_start = true + script = <<-EOT + #!/bin/bash + set -euo pipefail + trap 'coder exp sync complete post-claude' EXIT + coder exp sync want post-claude ${join(" ", module.claude-code.scripts)} + coder exp sync start post-claude -module "claude-code" { - source = "registry.coder.com/coder/claude-code/coder" - version = "4.9.2" - agent_id = coder_agent.main.id - workdir = "/home/coder/project" - claude_code_oauth_token = var.claude_code_oauth_token + # Your work here runs after claude-code finishes installing. + claude --version + EOT } ``` @@ -245,14 +207,12 @@ variable "aws_access_key_id" { type = string description = "Your AWS access key ID. Create this in the AWS IAM console under 'Security credentials'." sensitive = true - value = "xxxx-xxx-xxxx" } variable "aws_secret_access_key" { type = string description = "Your AWS secret access key. This is shown once when you create an access key in the AWS IAM console." sensitive = true - value = "xxxx-xxx-xxxx" } resource "coder_env" "aws_access_key_id" { @@ -273,7 +233,6 @@ variable "aws_bearer_token_bedrock" { type = string description = "Your AWS Bedrock bearer token. This provides access to Bedrock without needing separate access key and secret key." sensitive = true - value = "xxxx-xxx-xxxx" } resource "coder_env" "bedrock_api_key" { @@ -284,7 +243,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.9.2" + version = "5.0.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -341,7 +300,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.9.2" + version = "5.0.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" @@ -375,26 +334,17 @@ module "claude-code" { ## Troubleshooting -If you encounter any issues, check the log files in the `~/.claude-module` directory within your workspace for detailed information. +If you encounter any issues, check the log files in the `~/.coder-modules/coder/claude-code/logs` directory within your workspace for detailed information. ```bash # Installation logs -cat ~/.claude-module/install.log - -# Startup logs -cat ~/.claude-module/agentapi-start.log +cat ~/.coder-modules/coder/claude-code/logs/install.log # Pre/post install script logs -cat ~/.claude-module/pre_install.log -cat ~/.claude-module/post_install.log +cat ~/.coder-modules/coder/claude-code/logs/pre_install.log +cat ~/.coder-modules/coder/claude-code/logs/post_install.log ``` -> [!NOTE] -> To use tasks with Claude Code, you must provide an `anthropic_api_key` or `claude_code_oauth_token`. -> The `workdir` variable is required and specifies the directory where Claude Code will run. - ## References - [Claude Code Documentation](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) -- [AgentAPI Documentation](https://github.com/coder/agentapi) -- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents) diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index b01e88327..3cbe97920 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -6,15 +6,72 @@ import { beforeAll, expect, } from "bun:test"; -import { execContainer, readFileContainer, runTerraformInit } from "~test"; import { - loadTestFile, - writeExecutable, - setup as setupUtil, - execModuleScript, - expectAgentAPIStarted, -} from "../agentapi/test-util"; -import dedent from "dedent"; + execContainer, + readFileContainer, + removeContainer, + runContainer, + runTerraformApply, + runTerraformInit, + TerraformState, +} from "~test"; +import { extractCoderEnvVars, writeExecutable } from "../agentapi/test-util"; +import path from "path"; + +// coder-utils orchestrates this module's scripts and can produce multiple +// coder_script resources (pre_install, install, post_install). The shared +// `setup` helper in ../agentapi/test-util.ts assumes a single coder_script +// via findResourceInstance, so we define a local setup helper that collects +// every coder_script in run order. + +interface ModuleScripts { + pre_install?: string; + install: string; + post_install?: string; +} + +// Script display_names produced by coder-utils (Claude Code prefix + suffix). +// Order matters: scripts run sequentially in this order at agent startup. +const SCRIPT_SUFFIXES = [ + "Pre-Install Script", + "Install Script", + "Post-Install Script", +] as const; + +const collectScripts = (state: TerraformState): ModuleScripts => { + const byDisplayName: Record = {}; + for (const resource of state.resources) { + if (resource.type !== "coder_script") continue; + for (const instance of resource.instances) { + const attrs = instance.attributes as Record; + const displayName = attrs.display_name as string | undefined; + const script = attrs.script as string | undefined; + if (displayName && script) { + byDisplayName[displayName] = script; + } + } + } + const scripts: Partial = {}; + for (const suffix of SCRIPT_SUFFIXES) { + const key = `Claude Code: ${suffix}`; + if (!(key in byDisplayName)) continue; + switch (suffix) { + case "Pre-Install Script": + scripts.pre_install = byDisplayName[key]; + break; + case "Install Script": + scripts.install = byDisplayName[key]; + break; + case "Post-Install Script": + scripts.post_install = byDisplayName[key]; + break; + } + } + if (!scripts.install) { + throw new Error("install script not found in terraform state"); + } + return scripts as ModuleScripts; +}; let cleanupFunctions: (() => Promise)[] = []; const registerCleanup = (cleanup: () => Promise) => { @@ -33,37 +90,96 @@ afterEach(async () => { }); interface SetupProps { - skipAgentAPIMock?: boolean; skipClaudeMock?: boolean; moduleVariables?: Record; - agentapiMockScript?: string; } const setup = async ( props?: SetupProps, -): Promise<{ id: string; coderEnvVars: Record }> => { +): Promise<{ + id: string; + coderEnvVars: Record; + scripts: ModuleScripts; +}> => { const projectDir = "/home/coder/project"; - const { id, coderEnvVars } = await setupUtil({ - moduleDir: import.meta.dir, - moduleVariables: { - install_claude_code: props?.skipClaudeMock ? "true" : "false", - install_agentapi: props?.skipAgentAPIMock ? "true" : "false", - workdir: projectDir, - ...props?.moduleVariables, - }, - registerCleanup, - projectDir, - skipAgentAPIMock: props?.skipAgentAPIMock, - agentapiMockScript: props?.agentapiMockScript, + const moduleDir = path.resolve(import.meta.dir); + const state = await runTerraformApply(moduleDir, { + agent_id: "foo", + workdir: projectDir, + // Default to skipping the real installer; individual tests opt in. + install_claude_code: "false", + ...props?.moduleVariables, + }); + const scripts = collectScripts(state); + const coderEnvVars = extractCoderEnvVars(state); + + const id = await runContainer("codercom/enterprise-node:latest"); + registerCleanup(async () => { + if (process.env["DEBUG"] === "true" || process.env["DEBUG"] === "1") { + console.log(`Not removing container ${id} in debug mode`); + return; + } + await removeContainer(id); + }); + + await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]); + // Mock `coder` CLI so `coder exp sync` calls from coder-utils wrappers + // succeed without a real control plane. + await writeExecutable({ + containerId: id, + filePath: "/usr/bin/coder", + content: "#!/bin/bash\nexit 0\n", }); if (!props?.skipClaudeMock) { await writeExecutable({ containerId: id, filePath: "/usr/bin/claude", - content: await loadTestFile(import.meta.dir, "claude-mock.sh"), + content: await Bun.file( + path.join(moduleDir, "testdata", "claude-mock.sh"), + ).text(), + }); + } + return { id, coderEnvVars, scripts }; +}; + +// Runs the coder-utils script pipeline (pre_install, install, post_install) in +// order inside the container. Each script is written to /tmp and executed +// under bash with the test's env vars exported first. +const runScripts = async ( + id: string, + scripts: ModuleScripts, + env?: Record, +) => { + const entries = env ? Object.entries(env) : []; + const envArgs = + entries.length > 0 + ? entries + .map( + ([key, value]) => `export ${key}="${value.replace(/"/g, '\\"')}"`, + ) + .join(" && ") + " && " + : ""; + const ordered: [string, string | undefined][] = [ + ["pre_install", scripts.pre_install], + ["install", scripts.install], + ["post_install", scripts.post_install], + ]; + for (const [name, script] of ordered) { + if (!script) continue; + const target = `/tmp/coder-utils-${name}.sh`; + await writeExecutable({ + containerId: id, + filePath: target, + content: script, }); + const resp = await execContainer(id, ["bash", "-c", `${envArgs}${target}`]); + if (resp.exitCode !== 0) { + console.log(`script ${name} failed:`); + console.log(resp.stdout); + console.log(resp.stderr); + throw new Error(`coder-utils ${name} script exited ${resp.exitCode}`); + } } - return { id, coderEnvVars }; }; setDefaultTimeout(60 * 1000); @@ -74,56 +190,50 @@ describe("claude-code", async () => { }); test("happy-path", async () => { - const { id } = await setup(); - await execModuleScript(id); - await expectAgentAPIStarted(id); + const { id, scripts } = await setup(); + await runScripts(id, scripts); + const installLog = await readFileContainer( + id, + "/home/coder/.coder-modules/coder/claude-code/logs/install.log", + ); + expect(installLog).toContain("Skipping Claude Code installation"); }); test("install-claude-code-version", async () => { - const version_to_install = "1.0.40"; - const { id, coderEnvVars } = await setup({ + const version = "1.0.40"; + const { id, coderEnvVars, scripts } = await setup({ skipClaudeMock: true, moduleVariables: { install_claude_code: "true", - claude_code_version: version_to_install, + claude_code_version: version, }, }); - await execModuleScript(id, coderEnvVars); - const resp = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/install.log", - ]); - expect(resp.stdout).toContain(version_to_install); + await runScripts(id, scripts, coderEnvVars); + const installLog = await readFileContainer( + id, + "/home/coder/.coder-modules/coder/claude-code/logs/install.log", + ); + expect(installLog).toContain(version); }); - test("check-latest-claude-code-version-works", async () => { - const { id, coderEnvVars } = await setup({ - skipClaudeMock: true, - skipAgentAPIMock: true, + test("anthropic-api-key", async () => { + const apiKey = "test-api-key-123"; + const { coderEnvVars } = await setup({ moduleVariables: { - install_claude_code: "true", + anthropic_api_key: apiKey, }, }); - await execModuleScript(id, coderEnvVars); - await expectAgentAPIStarted(id); + expect(coderEnvVars["ANTHROPIC_API_KEY"]).toBe(apiKey); }); - test("claude-api-key", async () => { - const apiKey = "test-api-key-123"; - const { id } = await setup({ + test("claude-code-oauth-token", async () => { + const token = "test-oauth-token-456"; + const { coderEnvVars } = await setup({ moduleVariables: { - claude_api_key: apiKey, + claude_code_oauth_token: token, }, }); - await execModuleScript(id); - - const envCheck = await execContainer(id, [ - "bash", - "-c", - 'env | grep CLAUDE_API_KEY || echo "CLAUDE_API_KEY not found"', - ]); - expect(envCheck.stdout).toContain("CLAUDE_API_KEY"); + expect(coderEnvVars["CLAUDE_CODE_OAUTH_TOKEN"]).toBe(token); }); test("claude-mcp-config", async () => { @@ -135,349 +245,67 @@ describe("claude-code", async () => { }, }, }); - const { id, coderEnvVars } = await setup({ + const { id, coderEnvVars, scripts } = await setup({ skipClaudeMock: true, moduleVariables: { + install_claude_code: "true", mcp: mcpConfig, }, }); - await execModuleScript(id, coderEnvVars); - - const resp = await readFileContainer(id, "/home/coder/.claude.json"); - expect(resp).toContain("test-cmd"); - }); - - test("claude-task-prompt", async () => { - const prompt = "This is a task prompt for Claude."; - const { id } = await setup({ - moduleVariables: { - ai_prompt: prompt, - }, - }); - await execModuleScript(id); - - const resp = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - expect(resp.stdout).toContain(prompt); - }); - - test("claude-permission-mode", async () => { - const mode = "plan"; - const { id } = await setup({ - moduleVariables: { - permission_mode: mode, - 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(`--permission-mode ${mode}`); - }); - - test("claude-auto-permission-mode", async () => { - const mode = "auto"; - const { id } = await setup({ - moduleVariables: { - permission_mode: mode, - 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(`--permission-mode ${mode}`); + await runScripts(id, scripts, coderEnvVars); + const claudeConfig = await readFileContainer( + id, + "/home/coder/.claude.json", + ); + expect(claudeConfig).toContain("test-cmd"); }); test("claude-model", async () => { const model = "opus"; const { coderEnvVars } = await setup({ moduleVariables: { - model: model, - ai_prompt: "test prompt", + model, }, }); - - // Verify ANTHROPIC_MODEL env var is set via coder_env expect(coderEnvVars["ANTHROPIC_MODEL"]).toBe(model); }); - test("claude-continue-resume-task-session", async () => { - const { id } = await setup({ - moduleVariables: { - continue: "true", - report_tasks: "true", - ai_prompt: "test prompt", - }, - }); - - // Create a mock task session file with the hardcoded task session ID - // Note: Claude CLI creates files without "session-" prefix when using --session-id - const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; - const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; - await execContainer(id, ["mkdir", "-p", sessionDir]); - await execContainer(id, [ - "bash", - "-c", - `cat > ${sessionDir}/${taskSessionId}.jsonl << 'SESSIONEOF' -{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} -{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} -SESSIONEOF`, - ]); - - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - expect(startLog.stdout).toContain("--resume"); - expect(startLog.stdout).toContain(taskSessionId); - expect(startLog.stdout).toContain("Resuming task session"); - expect(startLog.stdout).toContain("--dangerously-skip-permissions"); - }); - test("pre-post-install-scripts", async () => { - const { id } = await setup({ + const { id, scripts } = await setup({ moduleVariables: { pre_install_script: "#!/bin/bash\necho 'claude-pre-install-script'", post_install_script: "#!/bin/bash\necho 'claude-post-install-script'", }, }); - await execModuleScript(id); + await runScripts(id, scripts); const preInstallLog = await readFileContainer( id, - "/home/coder/.claude-module/pre_install.log", + "/home/coder/.coder-modules/coder/claude-code/logs/pre_install.log", ); expect(preInstallLog).toContain("claude-pre-install-script"); const postInstallLog = await readFileContainer( id, - "/home/coder/.claude-module/post_install.log", + "/home/coder/.coder-modules/coder/claude-code/logs/post_install.log", ); expect(postInstallLog).toContain("claude-post-install-script"); }); test("workdir-variable", async () => { const workdir = "/home/coder/claude-test-folder"; - const { id } = await setup({ - skipClaudeMock: false, + const { id, scripts } = await setup({ moduleVariables: { workdir, }, }); - await execModuleScript(id); - - const resp = await readFileContainer( - id, - "/home/coder/.claude-module/agentapi-start.log", - ); - expect(resp).toContain(workdir); - }); - - test("coder-mcp-config-created", async () => { - const { id } = await setup({ - moduleVariables: { - install_claude_code: "false", - }, - }); - await execModuleScript(id); - + await runScripts(id, scripts); + // install.sh echoes ARG_WORKDIR and creates the directory if missing. const installLog = await readFileContainer( id, - "/home/coder/.claude-module/install.log", - ); - expect(installLog).toContain( - "Configuring Claude Code to report tasks via Coder MCP", - ); - }); - - test("dangerously-skip-permissions", async () => { - const { id } = await setup({ - moduleVariables: { - dangerously_skip_permissions: "true", - }, - }); - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - expect(startLog.stdout).toContain(`--dangerously-skip-permissions`); - }); - - test("subdomain-false", async () => { - const { id } = await setup({ - skipAgentAPIMock: true, - moduleVariables: { - subdomain: "false", - post_install_script: dedent` - #!/bin/bash - env | grep AGENTAPI_CHAT_BASE_PATH || echo "AGENTAPI_CHAT_BASE_PATH not found" - `, - }, - }); - - await execModuleScript(id); - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/post_install.log", - ]); - expect(startLog.stdout).toContain( - "ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat", - ); - }); - - test("partial-initialization-detection", async () => { - const { id } = await setup({ - moduleVariables: { - continue: "true", - report_tasks: "true", - ai_prompt: "test prompt", - }, - }); - - const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; - const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; - await execContainer(id, ["mkdir", "-p", sessionDir]); - - await execContainer(id, [ - "bash", - "-c", - `echo '{"sessionId":"${taskSessionId}"}' > ${sessionDir}/${taskSessionId}.jsonl`, - ]); - - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - - // Should start new session, not try to resume invalid one - expect(startLog.stdout).toContain("Starting new task session"); - expect(startLog.stdout).toContain("--session-id"); - }); - - test("standalone-first-build-no-sessions", async () => { - const { id } = await setup({ - moduleVariables: { - continue: "true", - report_tasks: "false", - }, - }); - - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - - // Should start fresh, not try to continue - expect(startLog.stdout).toContain("No sessions found"); - expect(startLog.stdout).toContain("starting fresh standalone session"); - expect(startLog.stdout).not.toContain("--continue"); - }); - - test("standalone-with-sessions-continues", async () => { - const { id } = await setup({ - moduleVariables: { - continue: "true", - report_tasks: "false", - }, - }); - - const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; - await execContainer(id, ["mkdir", "-p", sessionDir]); - await execContainer(id, [ - "bash", - "-c", - `cat > ${sessionDir}/generic-123.jsonl << 'EOF' -{"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2020-01-01T10:00:00.000Z"} -{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} -EOF`, - ]); - - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - - // Should continue existing session - expect(startLog.stdout).toContain("Sessions found"); - expect(startLog.stdout).toContain( - "Continuing most recent standalone session", + "/home/coder/.coder-modules/coder/claude-code/logs/install.log", ); - expect(startLog.stdout).toContain("--continue"); - }); - - test("task-mode-ignores-manual-sessions", async () => { - const { id } = await setup({ - moduleVariables: { - continue: "true", - report_tasks: "true", - ai_prompt: "test prompt", - }, - }); - - const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; - const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; - await execContainer(id, ["mkdir", "-p", sessionDir]); - - // Create task session (without "session-" prefix, as CLI does) - await execContainer(id, [ - "bash", - "-c", - `cat > ${sessionDir}/${taskSessionId}.jsonl << 'EOF' -{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} -{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} -EOF`, - ]); - - // Create manual session (newer) - await execContainer(id, [ - "bash", - "-c", - `cat > ${sessionDir}/manual-456.jsonl << 'EOF' -{"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2020-01-02T10:00:00.000Z"} -{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-02T10:00:05.000Z"} -EOF`, - ]); - - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - - // Should resume task session, not manual session - expect(startLog.stdout).toContain("Resuming task session"); - expect(startLog.stdout).toContain(taskSessionId); - expect(startLog.stdout).not.toContain("manual-456"); + expect(installLog).toContain(workdir); }); test("mcp-config-remote-path", async () => { @@ -485,43 +313,43 @@ EOF`, const successUrl = "https://raw.githubusercontent.com/coder/coder/main/.mcp.json"; - const { id, coderEnvVars } = await setup({ + const { id, coderEnvVars, scripts } = await setup({ skipClaudeMock: true, moduleVariables: { + install_claude_code: "true", mcp_config_remote_path: JSON.stringify([failingUrl, successUrl]), }, }); - await execModuleScript(id, coderEnvVars); + await runScripts(id, scripts, coderEnvVars); const installLog = await readFileContainer( id, - "/home/coder/.claude-module/install.log", + "/home/coder/.coder-modules/coder/claude-code/logs/install.log", ); - // Verify both URLs are attempted + // Verify both URLs are attempted. expect(installLog).toContain(failingUrl); expect(installLog).toContain(successUrl); - // First URL should fail gracefully + // First URL should fail gracefully. expect(installLog).toContain( `Warning: Failed to fetch MCP configuration from '${failingUrl}'`, ); - // Second URL should succeed - no failure warning for it + // Second URL should succeed. expect(installLog).not.toContain( `Warning: Failed to fetch MCP configuration from '${successUrl}'`, ); - // Should contain the MCP server add command from successful fetch + // Should contain the MCP server add command from the successful fetch. expect(installLog).toContain( "Added stdio MCP server go-language-server to local config", ); - expect(installLog).toContain( "Added stdio MCP server typescript-language-server to local config", ); - // Verify the MCP config was added to claude.json + // Verify the MCP config was added to .claude.json. const claudeConfig = await readFileContainer( id, "/home/coder/.claude.json", @@ -529,4 +357,82 @@ EOF`, expect(claudeConfig).toContain("typescript-language-server"); expect(claudeConfig).toContain("go-language-server"); }); + + test("standalone-mode-with-api-key", async () => { + const apiKey = "test-api-key-standalone"; + const workdir = "/home/coder/project"; + const { id, coderEnvVars, scripts } = await setup({ + moduleVariables: { + anthropic_api_key: apiKey, + }, + }); + await runScripts(id, scripts, coderEnvVars); + + const installLog = await readFileContainer( + id, + "/home/coder/.coder-modules/coder/claude-code/logs/install.log", + ); + expect(installLog).toContain("Configuring Claude Code for standalone mode"); + expect(installLog).toContain("Standalone mode configured successfully"); + + const claudeConfig = await readFileContainer( + id, + "/home/coder/.claude.json", + ); + const parsed = JSON.parse(claudeConfig); + expect(parsed.primaryApiKey).toBe(apiKey); + expect(parsed.autoUpdaterStatus).toBe("disabled"); + expect(parsed.hasCompletedOnboarding).toBe(true); + expect(parsed.bypassPermissionsModeAccepted).toBe(true); + expect(parsed.hasAcknowledgedCostThreshold).toBe(true); + expect(parsed.projects[workdir].hasCompletedProjectOnboarding).toBe(true); + expect(parsed.projects[workdir].hasTrustDialogAccepted).toBe(true); + }); + + test("standalone-mode-with-oauth-token", async () => { + const token = "test-oauth-token-standalone"; + const { id, coderEnvVars, scripts } = await setup({ + moduleVariables: { + claude_code_oauth_token: token, + }, + }); + await runScripts(id, scripts, coderEnvVars); + + const installLog = await readFileContainer( + id, + "/home/coder/.coder-modules/coder/claude-code/logs/install.log", + ); + expect(installLog).toContain("Standalone mode configured successfully"); + expect(installLog).not.toContain("skipping onboarding bypass"); + + // Onboarding bypass flags must be present; primaryApiKey is unused when + // auth happens via CLAUDE_CODE_OAUTH_TOKEN. + const claudeConfig = await readFileContainer( + id, + "/home/coder/.claude.json", + ); + const parsed = JSON.parse(claudeConfig); + expect(parsed.hasCompletedOnboarding).toBe(true); + expect(parsed.bypassPermissionsModeAccepted).toBe(true); + }); + + test("standalone-mode-no-auth", async () => { + const { id, coderEnvVars, scripts } = await setup(); + await runScripts(id, scripts, coderEnvVars); + + const installLog = await readFileContainer( + id, + "/home/coder/.coder-modules/coder/claude-code/logs/install.log", + ); + expect(installLog).toContain("No authentication configured"); + expect(installLog).toContain("skipping onboarding bypass"); + + // .claude.json should not exist when no auth is configured. + const resp = await execContainer(id, [ + "bash", + "-c", + "test -e /home/coder/.claude.json && echo EXISTS || echo ABSENT", + ]); + expect(resp.stdout.trim()).toBe("ABSENT"); + }); }); diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index db234c052..58f741729 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -18,18 +18,6 @@ data "coder_workspace" "me" {} data "coder_workspace_owner" "me" {} -variable "order" { - type = number - description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." - default = null -} - -variable "group" { - type = string - description = "The name of a group that this app belongs to." - default = null -} - variable "icon" { type = string description = "The icon to use for the app." @@ -38,37 +26,7 @@ variable "icon" { variable "workdir" { type = string - description = "The folder to run Claude Code in." -} - -variable "report_tasks" { - type = bool - description = "Whether to enable task reporting to Coder UI via AgentAPI" - default = true -} - -variable "web_app" { - type = bool - description = "Whether to create the web app for Claude Code. When false, AgentAPI still runs but no web UI app icon is shown in the Coder dashboard. This is automatically enabled when using Coder Tasks, regardless of this setting." - default = true -} - -variable "cli_app" { - type = bool - description = "Whether to create a CLI app for Claude Code" - default = false -} - -variable "web_app_display_name" { - type = string - description = "Display name for the web app" - default = "Claude Code" -} - -variable "cli_app_display_name" { - type = string - description = "Display name for the CLI app" - default = "Claude Code CLI" + description = "Project directory to pre-configure for Claude Code. The module creates this directory if it is missing, registers MCP servers against it, and pre-accepts the trust/onboarding prompts for it in ~/.claude.json." } variable "pre_install_script" { @@ -83,31 +41,6 @@ variable "post_install_script" { default = null } -variable "install_agentapi" { - type = bool - description = "Whether to install AgentAPI." - default = true -} - -variable "agentapi_version" { - type = string - description = "The version of AgentAPI to install." - default = "v0.11.8" -} - -variable "ai_prompt" { - type = string - description = "Initial task prompt for Claude Code." - default = "" -} - -variable "subdomain" { - type = bool - description = "Whether to use a subdomain for AgentAPI." - default = false -} - - variable "install_claude_code" { type = bool description = "Whether to install Claude Code." @@ -126,9 +59,9 @@ variable "disable_autoupdater" { default = false } -variable "claude_api_key" { +variable "anthropic_api_key" { type = string - description = "The API key to use for the Claude Code server." + description = "API key passed to Claude Code via the ANTHROPIC_API_KEY env var." default = "" } @@ -138,34 +71,6 @@ variable "model" { default = "" } -variable "resume_session_id" { - type = string - description = "Resume a specific session by ID." - default = "" -} - -variable "continue" { - type = bool - description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise runs prompt or starts new session. When false, always starts fresh (ignores existing sessions)." - default = true -} - -variable "dangerously_skip_permissions" { - type = bool - description = "Skip the permission prompts. Use with caution. This will be set to true if using Coder Tasks" - default = false -} - -variable "permission_mode" { - type = string - description = "Permission mode for the cli, check https://docs.anthropic.com/en/docs/claude-code/iam#permission-modes" - default = "" - validation { - condition = contains(["", "default", "acceptEdits", "plan", "auto", "bypassPermissions"], var.permission_mode) - error_message = "interaction_mode must be one of: default, acceptEdits, plan, auto, bypassPermissions." - } -} - variable "mcp" { type = string description = "MCP JSON to be added to the claude code local scope" @@ -178,38 +83,13 @@ variable "mcp_config_remote_path" { default = [] } -variable "allowed_tools" { - type = string - description = "A list of tools that should be allowed without prompting the user for permission, in addition to settings.json files." - default = "" -} - -variable "disallowed_tools" { - type = string - description = "A list of tools that should be disallowed without prompting the user for permission, in addition to settings.json files." - default = "" - -} - variable "claude_code_oauth_token" { type = string - description = "Set up a long-lived authentication token (requires Claude subscription). Generated using `claude setup-token` command" + description = "OAuth token passed to Claude Code via the CLAUDE_CODE_OAUTH_TOKEN env var. Generate one with `claude setup-token`." sensitive = true default = "" } -variable "system_prompt" { - type = string - description = "The system prompt to use for the Claude Code server." - default = "" -} - -variable "claude_md_path" { - type = string - description = "The path to 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." @@ -221,83 +101,43 @@ variable "claude_binary_path" { } } -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_ai_gateway" { type = bool - description = "Whether to enable coder boundary for network filtering" - default = false -} - -variable "boundary_version" { - type = string - description = "Boundary version. When use_boundary_directly is true, a release version should be provided or 'latest' for the latest release. When compile_boundary_from_source is true, a valid git reference should be provided (tag, commit, branch)." - default = "latest" -} - -variable "compile_boundary_from_source" { - type = bool - description = "Whether to compile boundary from source instead of using the official install script" - default = false -} - -variable "use_boundary_directly" { - type = bool - description = "Whether to use boundary binary directly instead of coder boundary subcommand. When false (default), uses coder boundary subcommand. When true, installs and uses boundary binary from release." - default = false -} - -variable "enable_aibridge" { - type = bool - description = "Use AI Bridge for Claude Code. https://coder.com/docs/ai-coder/ai-bridge" + description = "Use AI Gateway for Claude Code. https://coder.com/docs/ai-coder/ai-gateway" default = false validation { - condition = !(var.enable_aibridge && length(var.claude_api_key) > 0) - error_message = "claude_api_key cannot be provided when enable_aibridge is true. AI Bridge automatically authenticates the client using Coder credentials." + condition = !(var.enable_ai_gateway && length(var.anthropic_api_key) > 0) + error_message = "anthropic_api_key cannot be provided when enable_ai_gateway is true. AI Gateway automatically authenticates the client using Coder credentials." } validation { - condition = !(var.enable_aibridge && length(var.claude_code_oauth_token) > 0) - error_message = "claude_code_oauth_token cannot be provided when enable_aibridge is true. AI Bridge automatically authenticates the client using Coder credentials." + condition = !(var.enable_ai_gateway && length(var.claude_code_oauth_token) > 0) + error_message = "claude_code_oauth_token cannot be provided when enable_ai_gateway is true. AI Gateway automatically authenticates the client using Coder credentials." } } -variable "enable_state_persistence" { - type = bool - description = "Enable AgentAPI conversation state persistence across restarts." - default = true -} - -resource "coder_env" "claude_code_md_path" { - count = var.claude_md_path == "" ? 0 : 1 - agent_id = var.agent_id - name = "CODER_MCP_CLAUDE_MD_PATH" - value = var.claude_md_path -} - -resource "coder_env" "claude_code_system_prompt" { - agent_id = var.agent_id - name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT" - value = local.final_system_prompt -} - resource "coder_env" "claude_code_oauth_token" { + count = var.claude_code_oauth_token != "" ? 1 : 0 agent_id = var.agent_id name = "CLAUDE_CODE_OAUTH_TOKEN" value = var.claude_code_oauth_token } -resource "coder_env" "claude_api_key" { - count = (var.enable_aibridge || (var.claude_api_key != "")) ? 1 : 0 +resource "coder_env" "anthropic_api_key" { + count = var.anthropic_api_key != "" ? 1 : 0 + agent_id = var.agent_id + name = "ANTHROPIC_API_KEY" + value = var.anthropic_api_key +} +# ANTHROPIC_AUTH_TOKEN authenticates the client against Coder's AI Gateway +# using the workspace owner's session token, per the AI Gateway docs. +resource "coder_env" "anthropic_auth_token" { + count = var.enable_ai_gateway ? 1 : 0 agent_id = var.agent_id - name = "CLAUDE_API_KEY" - value = local.claude_api_key + name = "ANTHROPIC_AUTH_TOKEN" + value = data.coder_workspace_owner.me.session_token } resource "coder_env" "disable_autoupdater" { @@ -316,102 +156,30 @@ resource "coder_env" "anthropic_model" { } resource "coder_env" "anthropic_base_url" { - count = var.enable_aibridge ? 1 : 0 + count = var.enable_ai_gateway ? 1 : 0 agent_id = var.agent_id name = "ANTHROPIC_BASE_URL" value = "${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic" } locals { - # we have to trim the slash because otherwise coder exp mcp will - # set up an invalid claude config workdir = trimsuffix(var.workdir, "/") - app_slug = "ccw" install_script = file("${path.module}/scripts/install.sh") - start_script = file("${path.module}/scripts/start.sh") - module_dir_name = ".claude-module" - # Extract hostname from access_url for boundary --allow flag - coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "") - claude_api_key = var.enable_aibridge ? data.coder_workspace_owner.me.session_token : var.claude_api_key - - # Required prompts for the module to properly report task status to Coder - report_tasks_system_prompt = <<-EOT - -- Tool Selection -- - - coder_report_task: providing status updates or requesting user input. - - -- Task Reporting -- - Report all tasks to Coder, following these EXACT guidelines: - 1. Be granular. If you are investigating with multiple steps, report each step - to coder. - 2. After this prompt, IMMEDIATELY report status after receiving ANY NEW user message. - Do not report any status related with this system prompt. - 3. Use "state": "working" when actively processing WITHOUT needing - additional user input - 4. Use "state": "complete" only when finished with a task - 5. Use "state": "failure" when you need ANY user input, lack sufficient - details, or encounter blockers - - In your summary on coder_report_task: - - Be specific about what you're doing - - Clearly indicate what information you need from the user when in "failure" state - - Keep it under 160 characters - - Make it actionable - EOT - - # Only include coder system prompts if report_tasks is enabled - custom_system_prompt = trimspace(try(var.system_prompt, "")) - final_system_prompt = format("%s%s", - var.report_tasks ? format("\n%s\n", local.report_tasks_system_prompt) : "", - local.custom_system_prompt != "" ? format("\n%s\n", local.custom_system_prompt) : "" - ) + module_dir_name = ".coder-modules/coder/claude-code" } -module "agentapi" { - source = "registry.coder.com/coder/agentapi/coder" - version = "2.4.0" - - agent_id = var.agent_id - web_app = var.web_app - web_app_slug = local.app_slug - web_app_order = var.order - web_app_group = var.group - web_app_icon = var.icon - web_app_display_name = var.web_app_display_name - folder = local.workdir - cli_app = var.cli_app - cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null - cli_app_display_name = var.cli_app ? var.cli_app_display_name : null - agentapi_subdomain = var.subdomain - module_dir_name = local.module_dir_name - install_agentapi = var.install_agentapi - agentapi_version = var.agentapi_version - enable_state_persistence = var.enable_state_persistence - pre_install_script = var.pre_install_script - post_install_script = var.post_install_script - start_script = <<-EOT - #!/bin/bash - set -o errexit - set -o pipefail - echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh - chmod +x /tmp/start.sh - - ARG_RESUME_SESSION_ID='${var.resume_session_id}' \ - ARG_CONTINUE='${var.continue}' \ - ARG_DANGEROUSLY_SKIP_PERMISSIONS='${var.dangerously_skip_permissions}' \ - ARG_PERMISSION_MODE='${var.permission_mode}' \ - ARG_WORKDIR='${local.workdir}' \ - ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \ - ARG_REPORT_TASKS='${var.report_tasks}' \ - ARG_ENABLE_BOUNDARY='${var.enable_boundary}' \ - ARG_BOUNDARY_VERSION='${var.boundary_version}' \ - ARG_COMPILE_FROM_SOURCE='${var.compile_boundary_from_source}' \ - ARG_USE_BOUNDARY_DIRECTLY='${var.use_boundary_directly}' \ - ARG_CODER_HOST='${local.coder_host}' \ - ARG_CLAUDE_BINARY_PATH='${var.claude_binary_path}' \ - /tmp/start.sh - EOT +module "coder_utils" { + source = "registry.coder.com/coder/coder-utils/coder" + version = "1.3.0" - install_script = <<-EOT + agent_id = var.agent_id + agent_name = "claude-code" + module_directory = "$HOME/${local.module_dir_name}" + display_name_prefix = "Claude Code" + icon = var.icon + pre_install_script = var.pre_install_script + post_install_script = var.post_install_script + install_script = <<-EOT #!/bin/bash set -o errexit set -o pipefail @@ -419,22 +187,20 @@ 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_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}' \ - ARG_DISALLOWED_TOOLS='${var.disallowed_tools}' \ ARG_MCP='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \ ARG_MCP_CONFIG_REMOTE_PATH='${base64encode(jsonencode(var.mcp_config_remote_path))}' \ - ARG_ENABLE_AIBRIDGE='${var.enable_aibridge}' \ - ARG_PERMISSION_MODE='${var.permission_mode}' \ + ARG_ENABLE_AI_GATEWAY='${var.enable_ai_gateway}' \ /tmp/install.sh EOT } -output "task_app_id" { - value = module.agentapi.task_app_id +# Pass-through of coder-utils script outputs so upstream modules can serialize +# their coder_script resources behind this module's install pipeline using +# `coder exp sync want `. +output "scripts" { + description = "Ordered list of coder exp sync names for the coder_script resources this module actually creates, in run order (pre_install, install, post_install). Scripts that were not configured are absent from the list." + value = module.coder_utils.scripts } diff --git a/registry/coder/modules/claude-code/main.tftest.hcl b/registry/coder/modules/claude-code/main.tftest.hcl index 9c9df50f4..3b4798994 100644 --- a/registry/coder/modules/claude-code/main.tftest.hcl +++ b/registry/coder/modules/claude-code/main.tftest.hcl @@ -20,30 +20,20 @@ run "test_claude_code_basic" { condition = var.install_claude_code == true error_message = "Install claude_code should default to true" } - - assert { - condition = var.install_agentapi == true - error_message = "Install agentapi should default to true" - } - - assert { - condition = var.report_tasks == true - error_message = "report_tasks should default to true" - } } run "test_claude_code_with_api_key" { command = plan variables { - agent_id = "test-agent-456" - workdir = "/home/coder/workspace" - claude_api_key = "test-api-key-123" + agent_id = "test-agent-456" + workdir = "/home/coder/workspace" + anthropic_api_key = "test-api-key-123" } assert { - condition = coder_env.claude_api_key[0].value == "test-api-key-123" - error_message = "Claude API key value should match the input" + condition = coder_env.anthropic_api_key[0].value == "test-api-key-123" + error_message = "Anthropic API key value should match the input" } } @@ -51,30 +41,12 @@ run "test_claude_code_with_custom_options" { command = plan variables { - agent_id = "test-agent-789" - workdir = "/home/coder/custom" - order = 5 - group = "development" - icon = "/icon/custom.svg" - model = "opus" - ai_prompt = "Help me write better code" - permission_mode = "plan" - continue = true - install_claude_code = false - install_agentapi = false - claude_code_version = "1.0.0" - agentapi_version = "v0.6.0" - dangerously_skip_permissions = true - } - - assert { - condition = var.order == 5 - error_message = "Order variable should be set to 5" - } - - assert { - condition = var.group == "development" - error_message = "Group variable should be set to 'development'" + agent_id = "test-agent-789" + workdir = "/home/coder/custom" + icon = "/icon/custom.svg" + model = "opus" + install_claude_code = false + claude_code_version = "1.0.0" } assert { @@ -87,38 +59,13 @@ run "test_claude_code_with_custom_options" { error_message = "Claude model variable should be set to 'opus'" } - assert { - condition = var.ai_prompt == "Help me write better code" - error_message = "AI prompt variable should be set correctly" - } - - assert { - condition = var.permission_mode == "plan" - error_message = "Permission mode should be set to 'plan'" - } - - assert { - condition = var.continue == true - error_message = "Continue should be set to true" - } - assert { condition = var.claude_code_version == "1.0.0" error_message = "Claude Code version should be set to '1.0.0'" } - - assert { - condition = var.agentapi_version == "v0.6.0" - error_message = "AgentAPI version should be set to 'v0.6.0'" - } - - assert { - condition = var.dangerously_skip_permissions == true - error_message = "dangerously_skip_permissions should be set to true" - } } -run "test_claude_code_with_mcp_and_tools" { +run "test_claude_code_with_mcp" { command = plan variables { @@ -132,24 +79,12 @@ run "test_claude_code_with_mcp_and_tools" { } } }) - allowed_tools = "bash,python" - disallowed_tools = "rm" } assert { condition = var.mcp != "" error_message = "MCP configuration should be provided" } - - assert { - condition = var.allowed_tools == "bash,python" - error_message = "Allowed tools should be set" - } - - assert { - condition = var.disallowed_tools == "rm" - error_message = "Disallowed tools should be set" - } } run "test_claude_code_with_scripts" { @@ -173,144 +108,13 @@ run "test_claude_code_with_scripts" { } } -run "test_claude_code_permission_mode_validation" { +run "test_ai_gateway_enabled" { command = plan variables { - agent_id = "test-agent-validation" - workdir = "/home/coder/test" - permission_mode = "acceptEdits" - } - - assert { - condition = contains(["", "default", "acceptEdits", "plan", "auto", "bypassPermissions"], var.permission_mode) - error_message = "Permission mode should be one of the valid options" - } -} - -run "test_claude_code_auto_permission_mode" { - command = plan - - variables { - agent_id = "test-agent-auto" - workdir = "/home/coder/test" - permission_mode = "auto" - } - - assert { - condition = var.permission_mode == "auto" - error_message = "Permission mode should be set to auto" - } -} - -run "test_claude_code_with_boundary" { - command = plan - - variables { - agent_id = "test-agent-boundary" - workdir = "/home/coder/boundary-test" - enable_boundary = true - } - - assert { - condition = var.enable_boundary == true - error_message = "Boundary should be enabled" - } - - assert { - condition = local.coder_host != "" - error_message = "Coder host should be extracted from access URL" - } -} - -run "test_claude_code_system_prompt" { - command = plan - - variables { - agent_id = "test-agent-system-prompt" - workdir = "/home/coder/test" - system_prompt = "Custom addition" - } - - assert { - condition = trimspace(coder_env.claude_code_system_prompt.value) != "" - error_message = "System prompt should not be empty" - } - - assert { - condition = length(regexall("Custom addition", coder_env.claude_code_system_prompt.value)) > 0 - error_message = "System prompt should have system_prompt variable value" - } -} - -run "test_claude_report_tasks_default" { - command = plan - - variables { - agent_id = "test-agent-report-tasks" - workdir = "/home/coder/test" - # report_tasks: default is true - } - - assert { - condition = trimspace(coder_env.claude_code_system_prompt.value) != "" - error_message = "System prompt should not be empty" - } - - # Ensure system prompt is wrapped by - assert { - condition = startswith(trimspace(coder_env.claude_code_system_prompt.value), "") - error_message = "System prompt should start with " - } - assert { - condition = endswith(trimspace(coder_env.claude_code_system_prompt.value), "") - error_message = "System prompt should end with " - } - - # Ensure Coder sections are injected when report_tasks=true (default) - assert { - condition = length(regexall("-- Tool Selection --", coder_env.claude_code_system_prompt.value)) > 0 - error_message = "System prompt should have Tool Selection section" - } - - assert { - condition = length(regexall("-- Task Reporting --", coder_env.claude_code_system_prompt.value)) > 0 - error_message = "System prompt should have Task Reporting section" - } -} - -run "test_claude_report_tasks_disabled" { - command = plan - - variables { - agent_id = "test-agent-report-tasks" - workdir = "/home/coder/test" - report_tasks = false - } - - assert { - condition = trimspace(coder_env.claude_code_system_prompt.value) != "" - error_message = "System prompt should not be empty" - } - - # Ensure system prompt is wrapped by - assert { - condition = startswith(trimspace(coder_env.claude_code_system_prompt.value), "") - error_message = "System prompt should start with " - } - assert { - condition = endswith(trimspace(coder_env.claude_code_system_prompt.value), "") - error_message = "System prompt should end with " - } -} - -run "test_aibridge_enabled" { - command = plan - - variables { - agent_id = "test-agent-aibridge" - workdir = "/home/coder/aibridge" - enable_aibridge = true + agent_id = "test-agent-ai-gateway" + workdir = "/home/coder/ai-gateway" + enable_ai_gateway = true } override_data { @@ -321,8 +125,8 @@ run "test_aibridge_enabled" { } assert { - condition = var.enable_aibridge == true - error_message = "AI Bridge should be enabled" + condition = var.enable_ai_gateway == true + error_message = "AI Gateway should be enabled" } assert { @@ -332,131 +136,137 @@ run "test_aibridge_enabled" { assert { condition = length(regexall("/api/v2/aibridge/anthropic", coder_env.anthropic_base_url[0].value)) > 0 - error_message = "ANTHROPIC_BASE_URL should point to AI Bridge endpoint" + error_message = "ANTHROPIC_BASE_URL should point to AI Gateway endpoint" } assert { - condition = coder_env.claude_api_key[0].name == "CLAUDE_API_KEY" - error_message = "CLAUDE_API_KEY environment variable should be set" + condition = coder_env.anthropic_auth_token[0].name == "ANTHROPIC_AUTH_TOKEN" + error_message = "ANTHROPIC_AUTH_TOKEN environment variable should be set" } assert { - condition = coder_env.claude_api_key[0].value == data.coder_workspace_owner.me.session_token - error_message = "CLAUDE_API_KEY should use workspace owner's session token when aibridge is enabled" + condition = coder_env.anthropic_auth_token[0].value == data.coder_workspace_owner.me.session_token + error_message = "ANTHROPIC_AUTH_TOKEN should use workspace owner's session token when ai_gateway is enabled" + } + + assert { + condition = length(coder_env.anthropic_api_key) == 0 + error_message = "ANTHROPIC_API_KEY env should not be created when ai_gateway is enabled and no anthropic_api_key is provided" } } -run "test_aibridge_validation_with_api_key" { +run "test_ai_gateway_validation_with_api_key" { command = plan variables { - agent_id = "test-agent-validation" - workdir = "/home/coder/test" - enable_aibridge = true - claude_api_key = "test-api-key" + agent_id = "test-agent-validation" + workdir = "/home/coder/test" + enable_ai_gateway = true + anthropic_api_key = "test-api-key" } expect_failures = [ - var.enable_aibridge, + var.enable_ai_gateway, ] } -run "test_aibridge_validation_with_oauth_token" { +run "test_ai_gateway_validation_with_oauth_token" { command = plan variables { agent_id = "test-agent-validation" workdir = "/home/coder/test" - enable_aibridge = true - claude_code_oauth_token = "test-oauth-token" + enable_ai_gateway = true + claude_code_oauth_token = "test-auth-token" } expect_failures = [ - var.enable_aibridge, + var.enable_ai_gateway, ] } -run "test_aibridge_disabled_with_api_key" { +run "test_ai_gateway_disabled_with_api_key" { command = plan variables { - agent_id = "test-agent-no-aibridge" - workdir = "/home/coder/test" - enable_aibridge = false - claude_api_key = "test-api-key-xyz" + agent_id = "test-agent-no-ai-gateway" + workdir = "/home/coder/test" + enable_ai_gateway = false + anthropic_api_key = "test-api-key-xyz" } assert { - condition = var.enable_aibridge == false - error_message = "AI Bridge should be disabled" + condition = var.enable_ai_gateway == false + error_message = "AI Gateway should be disabled" } assert { - condition = coder_env.claude_api_key[0].value == "test-api-key-xyz" - error_message = "CLAUDE_API_KEY should use the provided API key when aibridge is disabled" + condition = coder_env.anthropic_api_key[0].value == "test-api-key-xyz" + error_message = "ANTHROPIC_API_KEY should use the provided API key when ai_gateway is disabled" } assert { condition = length(coder_env.anthropic_base_url) == 0 - error_message = "ANTHROPIC_BASE_URL should not be set when aibridge is disabled" + error_message = "ANTHROPIC_BASE_URL should not be set when ai_gateway is disabled" } } -run "test_enable_state_persistence_default" { +run "test_no_api_key_no_env" { command = plan variables { - agent_id = "test-agent" - workdir = "/home/coder" + agent_id = "test-agent-no-key" + workdir = "/home/coder/test" + enable_ai_gateway = false } assert { - condition = var.enable_state_persistence == true - error_message = "enable_state_persistence should default to true" + condition = length(coder_env.anthropic_api_key) == 0 + error_message = "ANTHROPIC_API_KEY should not be created when no API key is provided and ai_gateway is disabled" } } -run "test_disable_state_persistence" { +run "test_api_key_count_with_ai_gateway_no_override" { command = plan variables { - agent_id = "test-agent" - workdir = "/home/coder" - enable_state_persistence = false + agent_id = "test-agent-count" + workdir = "/home/coder/test" + enable_ai_gateway = true } assert { - condition = var.enable_state_persistence == false - error_message = "enable_state_persistence should be false when explicitly disabled" + condition = length(coder_env.anthropic_auth_token) == 1 + error_message = "ANTHROPIC_AUTH_TOKEN env should be created when ai_gateway is enabled" } } -run "test_no_api_key_no_env" { +run "test_script_outputs_install_only" { command = plan variables { - agent_id = "test-agent-no-key" - workdir = "/home/coder/test" - enable_aibridge = false + agent_id = "test-agent-outputs" + workdir = "/home/coder/test" } assert { - condition = length(coder_env.claude_api_key) == 0 - error_message = "CLAUDE_API_KEY should not be created when no API key is provided and aibridge is disabled" + condition = length(output.scripts) == 1 && output.scripts[0] == "claude-code-install_script" + error_message = "scripts output should list only the install script when pre/post are not configured" } } -run "test_api_key_count_with_aibridge_no_override" { +run "test_script_outputs_with_pre_and_post" { command = plan variables { - agent_id = "test-agent-count" - workdir = "/home/coder/test" - enable_aibridge = true + agent_id = "test-agent-outputs-all" + workdir = "/home/coder/test" + pre_install_script = "echo pre" + post_install_script = "echo post" } assert { - condition = length(coder_env.claude_api_key) == 1 - error_message = "CLAUDE_API_KEY env should be created when aibridge is enabled, regardless of session_token value" + condition = output.scripts == ["claude-code-pre_install_script", "claude-code-install_script", "claude-code-post_install_script"] + error_message = "scripts output should list pre_install, install, post_install in run order" } -} \ No newline at end of file +} diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index c00773b5e..997602eba 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -14,15 +14,9 @@ ARG_INSTALL_CLAUDE_CODE=${ARG_INSTALL_CLAUDE_CODE:-} ARG_CLAUDE_BINARY_PATH=${ARG_CLAUDE_BINARY_PATH:-"$HOME/.local/bin"} ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH/#\~/$HOME}" ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH//\$HOME/$HOME}" -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) ARG_MCP_CONFIG_REMOTE_PATH=$(echo -n "${ARG_MCP_CONFIG_REMOTE_PATH:-}" | base64 -d) -ARG_ALLOWED_TOOLS=${ARG_ALLOWED_TOOLS:-} -ARG_DISALLOWED_TOOLS=${ARG_DISALLOWED_TOOLS:-} -ARG_ENABLE_AIBRIDGE=${ARG_ENABLE_AIBRIDGE:-false} -ARG_PERMISSION_MODE=${ARG_PERMISSION_MODE:-} +ARG_ENABLE_AI_GATEWAY=${ARG_ENABLE_AI_GATEWAY:-false} export PATH="$ARG_CLAUDE_BINARY_PATH:$PATH" @@ -32,14 +26,9 @@ 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" printf "ARG_MCP_CONFIG_REMOTE_PATH: %s\n" "$ARG_MCP_CONFIG_REMOTE_PATH" -printf "ARG_ALLOWED_TOOLS: %s\n" "$ARG_ALLOWED_TOOLS" -printf "ARG_DISALLOWED_TOOLS: %s\n" "$ARG_DISALLOWED_TOOLS" -printf "ARG_ENABLE_AIBRIDGE: %s\n" "$ARG_ENABLE_AIBRIDGE" +printf "ARG_ENABLE_AI_GATEWAY: %s\n" "$ARG_ENABLE_AI_GATEWAY" echo "--------------------------------" @@ -110,23 +99,15 @@ function install_claude_code_cli() { return fi - # Use npm when install_via_npm is true - if [ "$ARG_INSTALL_VIA_NPM" = "true" ]; then - echo "WARNING: npm installation method will be deprecated and removed in the next major release." - 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" - fi - echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')" + 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" fi + echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')" ensure_claude_in_path } @@ -139,7 +120,7 @@ function setup_claude_configurations() { echo "Folder created successfully." fi - module_path="$HOME/.claude-module" + module_path="$HOME/.coder-modules/coder/claude-code" mkdir -p "$module_path" if [ "$ARG_MCP" != "" ]; then @@ -167,34 +148,24 @@ function setup_claude_configurations() { ) fi - if [ -n "$ARG_ALLOWED_TOOLS" ]; then - coder --allowedTools "$ARG_ALLOWED_TOOLS" - fi - - if [ -n "$ARG_DISALLOWED_TOOLS" ]; then - coder --disallowedTools "$ARG_DISALLOWED_TOOLS" - fi - } 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 "${ANTHROPIC_API_KEY:-}" ] && [ -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ] && [ "$ARG_ENABLE_AI_GATEWAY" = "false" ]; then + echo "Note: No authentication configured (anthropic_api_key, claude_code_oauth_token, enable_ai_gateway), skipping onboarding bypass" 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 if [ -f "$claude_config" ]; then echo "Updating existing Claude configuration at $claude_config" - jq --arg workdir "$ARG_WORKDIR" --arg apikey "${CLAUDE_API_KEY:-}" \ + jq --arg workdir "$ARG_WORKDIR" --arg apikey "${ANTHROPIC_API_KEY:-}" \ '.autoUpdaterStatus = "disabled" | .autoModeAccepted = true | .bypassPermissionsModeAccepted = true | @@ -213,7 +184,7 @@ function configure_standalone_mode() { "bypassPermissionsModeAccepted": true, "hasAcknowledgedCostThreshold": true, "hasCompletedOnboarding": true, - "primaryApiKey": "${CLAUDE_API_KEY:-}", + "primaryApiKey": "${ANTHROPIC_API_KEY:-}", "projects": { "$ARG_WORKDIR": { "hasCompletedProjectOnboarding": true, @@ -227,39 +198,6 @@ EOF echo "Standalone mode configured successfully" } -function report_tasks() { - if [ "$ARG_REPORT_TASKS" = "true" ]; then - echo "Configuring Claude Code to report tasks via Coder MCP..." - export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG" - export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284" - coder exp mcp configure claude-code "$ARG_WORKDIR" - else - configure_standalone_mode - fi -} - -function accept_auto_mode() { - # Pre-accept the auto mode TOS prompt so it doesn't appear interactively. - # Claude Code shows a confirmation dialog for auto mode that blocks - # non-interactive/headless usage. - # Note: bypassPermissions acceptance is already handled by - # coder exp mcp configure (task mode) and configure_standalone_mode. - local claude_config="$HOME/.claude.json" - - if [ -f "$claude_config" ]; then - jq '.autoModeAccepted = true' \ - "$claude_config" > "${claude_config}.tmp" && mv "${claude_config}.tmp" "$claude_config" - else - echo '{"autoModeAccepted": true}' > "$claude_config" - fi - - echo "Pre-accepted auto mode prompt" -} - install_claude_code_cli setup_claude_configurations -report_tasks - -if [ "$ARG_PERMISSION_MODE" = "auto" ]; then - accept_auto_mode -fi +configure_standalone_mode diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh deleted file mode 100644 index 5ccbc8fa1..000000000 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ /dev/null @@ -1,256 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -ARG_CLAUDE_BINARY_PATH=${ARG_CLAUDE_BINARY_PATH:-"$HOME/.local/bin"} -ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH/#\~/$HOME}" -ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH//\$HOME/$HOME}" - -export PATH="$ARG_CLAUDE_BINARY_PATH:$PATH" - -command_exists() { - command -v "$1" > /dev/null 2>&1 -} - -ARG_RESUME_SESSION_ID=${ARG_RESUME_SESSION_ID:-} -ARG_CONTINUE=${ARG_CONTINUE:-false} -ARG_DANGEROUSLY_SKIP_PERMISSIONS=${ARG_DANGEROUSLY_SKIP_PERMISSIONS:-} -ARG_PERMISSION_MODE=${ARG_PERMISSION_MODE:-} -ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"} -ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d) -ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true} -ARG_ENABLE_BOUNDARY=${ARG_ENABLE_BOUNDARY:-false} -ARG_BOUNDARY_VERSION=${ARG_BOUNDARY_VERSION:-"latest"} -ARG_COMPILE_FROM_SOURCE=${ARG_COMPILE_FROM_SOURCE:-false} -ARG_USE_BOUNDARY_DIRECTLY=${ARG_USE_BOUNDARY_DIRECTLY:-false} -ARG_CODER_HOST=${ARG_CODER_HOST:-} - -echo "--------------------------------" - -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" -printf "ARG_PERMISSION_MODE: %s\n" "$ARG_PERMISSION_MODE" -printf "ARG_AI_PROMPT: %s\n" "$ARG_AI_PROMPT" -printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR" -printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS" -printf "ARG_ENABLE_BOUNDARY: %s\n" "$ARG_ENABLE_BOUNDARY" -printf "ARG_BOUNDARY_VERSION: %s\n" "$ARG_BOUNDARY_VERSION" -printf "ARG_COMPILE_FROM_SOURCE: %s\n" "$ARG_COMPILE_FROM_SOURCE" -printf "ARG_USE_BOUNDARY_DIRECTLY: %s\n" "$ARG_USE_BOUNDARY_DIRECTLY" -printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST" - -echo "--------------------------------" - -function install_boundary() { - if [ "$ARG_COMPILE_FROM_SOURCE" = "true" ]; then - # Install boundary by compiling from source - echo "Compiling boundary from source (version: $ARG_BOUNDARY_VERSION)" - - echo "Removing existing boundary directory to allow re-running the script safely" - if [ -d boundary ]; then - rm -rf boundary - fi - - echo "Clone boundary repository" - git clone https://github.com/coder/boundary.git - cd boundary - git checkout "$ARG_BOUNDARY_VERSION" - - # Build the binary - make build - - # Install binary - sudo cp boundary /usr/local/bin/ - sudo chmod +x /usr/local/bin/boundary - elif [ "$ARG_USE_BOUNDARY_DIRECTLY" = "true" ]; then - # Install boundary using official install script - echo "Installing boundary using official install script (version: $ARG_BOUNDARY_VERSION)" - curl -fsSL https://raw.githubusercontent.com/coder/boundary/main/install.sh | bash -s -- --version "$ARG_BOUNDARY_VERSION" - else - # Use coder boundary subcommand (default) - no installation needed - echo "Using coder boundary subcommand (provided by Coder)" - fi -} - -function validate_claude_installation() { - if command_exists claude; then - printf "Claude Code is installed\n" - else - printf "Error: Claude Code is not installed. Please enable install_claude_code or install it manually\n" - exit 1 - fi -} - -# Hardcoded task session ID for Coder task reporting -# This ensures all task sessions use a consistent, predictable ID -TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2" - -get_project_dir() { - local workdir_normalized - workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/._' '-') - echo "$HOME/.claude/projects/${workdir_normalized}" -} - -get_task_session_file() { - echo "$(get_project_dir)/${TASK_SESSION_ID}.jsonl" -} - -task_session_exists() { - local session_file - session_file=$(get_task_session_file) - - if [ -f "$session_file" ]; then - printf "Task session file found: %s\n" "$session_file" - return 0 - else - printf "Task session file not found: %s\n" "$session_file" - return 1 - fi -} - -is_valid_session() { - local session_file="$1" - - # Check if file exists and is not empty - # Empty files indicate the session was created but never used so they need to be removed - if [ ! -f "$session_file" ]; then - printf "Session validation failed: file does not exist\n" - return 1 - fi - - if [ ! -s "$session_file" ]; then - printf "Session validation failed: file is empty, removing stale file\n" - rm -f "$session_file" - return 1 - fi - - # Check for minimum session content - # Valid sessions need at least 2 lines: initial message and first response - local line_count - line_count=$(wc -l < "$session_file") - if [ "$line_count" -lt 2 ]; then - printf "Session validation failed: incomplete (only %s lines), removing incomplete file\n" "$line_count" - rm -f "$session_file" - return 1 - fi - - # Validate JSONL format by checking first 3 lines - # Claude session files use JSONL (JSON Lines) format where each line is valid JSON - if ! head -3 "$session_file" | jq empty 2> /dev/null; then - printf "Session validation failed: invalid JSONL format, removing corrupt file\n" - rm -f "$session_file" - return 1 - fi - - # Verify the session has a valid sessionId field - # This ensures the file structure matches Claude's session format - if ! grep -q '"sessionId"' "$session_file" \ - || ! grep -m 1 '"sessionId"' "$session_file" | jq -e '.sessionId' > /dev/null 2>&1; then - printf "Session validation failed: no valid sessionId found, removing malformed file\n" - rm -f "$session_file" - return 1 - fi - - printf "Session validation passed: %s\n" "$session_file" - return 0 -} - -has_any_sessions() { - local project_dir - project_dir=$(get_project_dir) - - if [ -d "$project_dir" ] && find "$project_dir" -maxdepth 1 -name "*.jsonl" -size +0c 2> /dev/null | grep -q .; then - printf "Sessions found in: %s\n" "$project_dir" - return 0 - else - printf "No sessions found in: %s\n" "$project_dir" - return 1 - fi -} - -ARGS=() - -function start_agentapi() { - # For Task reporting - export CODER_MCP_ALLOWED_TOOLS="coder_report_task" - - mkdir -p "$ARG_WORKDIR" - cd "$ARG_WORKDIR" - - if [ -n "$ARG_PERMISSION_MODE" ]; then - ARGS+=(--permission-mode "$ARG_PERMISSION_MODE") - fi - - if [ -n "$ARG_RESUME_SESSION_ID" ]; then - echo "Resuming specified session: $ARG_RESUME_SESSION_ID" - ARGS+=(--resume "$ARG_RESUME_SESSION_ID") - [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) - - elif [ "$ARG_CONTINUE" = "true" ]; then - - if [ "$ARG_REPORT_TASKS" = "true" ]; then - local session_file - session_file=$(get_task_session_file) - - if task_session_exists && is_valid_session "$session_file"; then - echo "Resuming task session: $TASK_SESSION_ID" - ARGS+=(--resume "$TASK_SESSION_ID" --dangerously-skip-permissions) - else - echo "Starting new task session: $TASK_SESSION_ID" - ARGS+=(--session-id "$TASK_SESSION_ID" --dangerously-skip-permissions) - [ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT") - fi - - else - if has_any_sessions; then - echo "Continuing most recent standalone session" - ARGS+=(--continue) - [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) - else - echo "No sessions found, starting fresh standalone session" - [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) - [ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT") - fi - fi - - else - echo "Continue disabled, starting fresh session" - [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions) - [ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT") - fi - - printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")" - - if [ "$ARG_ENABLE_BOUNDARY" = "true" ]; then - install_boundary - - printf "Starting with coder boundary enabled\n" - - BOUNDARY_ARGS+=() - - # Determine which boundary command to use - if [ "$ARG_COMPILE_FROM_SOURCE" = "true" ] || [ "$ARG_USE_BOUNDARY_DIRECTLY" = "true" ]; then - # Use boundary binary directly (from compilation or release installation) - BOUNDARY_CMD=("boundary") - else - # Use coder boundary subcommand (default) - # Copy coder binary to coder-no-caps. Copying strips CAP_NET_ADMIN capabilities - # from the binary, which is necessary because boundary doesn't work with - # privileged binaries (you can't launch privileged binaries inside network - # namespaces unless you have sys_admin). - CODER_NO_CAPS="$(dirname "$(which coder)")/coder-no-caps" - cp "$(which coder)" "$CODER_NO_CAPS" - BOUNDARY_CMD=("$CODER_NO_CAPS" "boundary") - fi - - agentapi server --type claude --term-width 67 --term-height 1190 -- \ - "${BOUNDARY_CMD[@]}" "${BOUNDARY_ARGS[@]}" -- \ - claude "${ARGS[@]}" - else - agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}" - fi -} - -validate_claude_installation -start_agentapi diff --git a/registry/coder/modules/claude-code/testdata/claude-mock.sh b/registry/coder/modules/claude-code/testdata/claude-mock.sh index b437b4d30..7f816e17a 100644 --- a/registry/coder/modules/claude-code/testdata/claude-mock.sh +++ b/registry/coder/modules/claude-code/testdata/claude-mock.sh @@ -5,9 +5,6 @@ if [[ "$1" == "--version" ]]; then exit 0 fi -set -e - -while true; do - echo "$(date) - claude-mock" - sleep 15 -done +# Mirror invocation for test assertions and exit cleanly. +echo "claude invoked with: $*" +exit 0 From c9bd6d02a5672878e7cf53c6c7ee52c45672b89b Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 24 Apr 2026 07:50:12 +0000 Subject: [PATCH 2/4] refactor(registry/coder/modules/claude-code): render install script via templatefile Render scripts/install.sh.tftpl once at plan time and pass the result directly to coder-utils, replacing the runtime base64 encode/decode wrapper and the redundant scripts/install.sh. Sources coder-utils from a pinned commit on its refactor branch until the `module_directory`-derived caller name lands in a tagged release. --- .../coder/modules/claude-code/main.test.ts | 2 +- registry/coder/modules/claude-code/main.tf | 34 ++- .../coder/modules/claude-code/main.tftest.hcl | 4 +- .../modules/claude-code/scripts/install.sh | 203 ------------------ .../claude-code/scripts/install.sh.tftpl | 201 +++++++++++++++++ 5 files changed, 217 insertions(+), 227 deletions(-) delete mode 100644 registry/coder/modules/claude-code/scripts/install.sh create mode 100644 registry/coder/modules/claude-code/scripts/install.sh.tftpl diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index 3cbe97920..486f7a997 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -300,7 +300,7 @@ describe("claude-code", async () => { }, }); await runScripts(id, scripts); - // install.sh echoes ARG_WORKDIR and creates the directory if missing. + // install.sh.tftpl echoes ARG_WORKDIR and creates the directory if missing. const installLog = await readFileContainer( id, "/home/coder/.coder-modules/coder/claude-code/logs/install.log", diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 58f741729..debb6f1b8 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -73,7 +73,7 @@ variable "model" { variable "mcp" { type = string - description = "MCP JSON to be added to the claude code local scope" + description = "JSON-encoded string to configure MCP servers for Claude Code. When set, writes MCP configuration into the Claude Code local scope." default = "" } @@ -163,38 +163,30 @@ resource "coder_env" "anthropic_base_url" { } locals { - workdir = trimsuffix(var.workdir, "/") - install_script = file("${path.module}/scripts/install.sh") + workdir = trimsuffix(var.workdir, "/") + install_script = templatefile("${path.module}/scripts/install.sh.tftpl", { + ARG_CLAUDE_CODE_VERSION = var.claude_code_version + ARG_INSTALL_CLAUDE_CODE = tostring(var.install_claude_code) + ARG_CLAUDE_BINARY_PATH = var.claude_binary_path + ARG_WORKDIR = local.workdir + ARG_MCP = var.mcp != "" ? base64encode(var.mcp) : "" + ARG_MCP_CONFIG_REMOTE_PATH = base64encode(jsonencode(var.mcp_config_remote_path)) + ARG_ENABLE_AI_GATEWAY = tostring(var.enable_ai_gateway) + }) module_dir_name = ".coder-modules/coder/claude-code" } module "coder_utils" { source = "registry.coder.com/coder/coder-utils/coder" - version = "1.3.0" + version = "0.0.1" agent_id = var.agent_id - agent_name = "claude-code" module_directory = "$HOME/${local.module_dir_name}" display_name_prefix = "Claude Code" icon = var.icon pre_install_script = var.pre_install_script post_install_script = var.post_install_script - install_script = <<-EOT - #!/bin/bash - set -o errexit - set -o pipefail - - 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_INSTALL_CLAUDE_CODE='${var.install_claude_code}' \ - ARG_CLAUDE_BINARY_PATH='${var.claude_binary_path}' \ - ARG_WORKDIR='${local.workdir}' \ - ARG_MCP='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \ - ARG_MCP_CONFIG_REMOTE_PATH='${base64encode(jsonencode(var.mcp_config_remote_path))}' \ - ARG_ENABLE_AI_GATEWAY='${var.enable_ai_gateway}' \ - /tmp/install.sh - EOT + install_script = local.install_script } # Pass-through of coder-utils script outputs so upstream modules can serialize diff --git a/registry/coder/modules/claude-code/main.tftest.hcl b/registry/coder/modules/claude-code/main.tftest.hcl index 3b4798994..071810c73 100644 --- a/registry/coder/modules/claude-code/main.tftest.hcl +++ b/registry/coder/modules/claude-code/main.tftest.hcl @@ -250,7 +250,7 @@ run "test_script_outputs_install_only" { } assert { - condition = length(output.scripts) == 1 && output.scripts[0] == "claude-code-install_script" + condition = length(output.scripts) == 1 && output.scripts[0] == "coder-claude-code-install_script" error_message = "scripts output should list only the install script when pre/post are not configured" } } @@ -266,7 +266,7 @@ run "test_script_outputs_with_pre_and_post" { } assert { - condition = output.scripts == ["claude-code-pre_install_script", "claude-code-install_script", "claude-code-post_install_script"] + condition = output.scripts == ["coder-claude-code-pre_install_script", "coder-claude-code-install_script", "coder-claude-code-post_install_script"] error_message = "scripts output should list pre_install, install, post_install in run order" } } diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh deleted file mode 100644 index 997602eba..000000000 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ /dev/null @@ -1,203 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -BOLD='\033[0;1m' - -command_exists() { - command -v "$1" > /dev/null 2>&1 -} - -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_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH/#\~/$HOME}" -ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH//\$HOME/$HOME}" -ARG_MCP=$(echo -n "${ARG_MCP:-}" | base64 -d) -ARG_MCP_CONFIG_REMOTE_PATH=$(echo -n "${ARG_MCP_CONFIG_REMOTE_PATH:-}" | base64 -d) -ARG_ENABLE_AI_GATEWAY=${ARG_ENABLE_AI_GATEWAY:-false} - -export PATH="$ARG_CLAUDE_BINARY_PATH:$PATH" - -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_MCP: %s\n" "$ARG_MCP" -printf "ARG_MCP_CONFIG_REMOTE_PATH: %s\n" "$ARG_MCP_CONFIG_REMOTE_PATH" -printf "ARG_ENABLE_AI_GATEWAY: %s\n" "$ARG_ENABLE_AI_GATEWAY" - -echo "--------------------------------" - -function add_mcp_servers() { - local mcp_json="$1" - local source_desc="$2" - - while IFS= read -r server_name && IFS= read -r server_json; do - echo "------------------------" - echo "Executing: claude mcp add-json \"$server_name\" '$server_json' ($source_desc)" - claude mcp add-json "$server_name" "$server_json" || echo "Warning: Failed to add MCP server '$server_name', continuing..." - echo "------------------------" - echo "" - done < <(echo "$mcp_json" | jq -r '.mcpServers | to_entries[] | .key, (.value | @json)') -} - -function add_path_to_shell_profiles() { - local path_dir="$1" - - for profile in "$HOME/.profile" "$HOME/.bash_profile" "$HOME/.bashrc" "$HOME/.zprofile" "$HOME/.zshrc"; do - if [ -f "$profile" ]; then - if ! grep -q "$path_dir" "$profile" 2> /dev/null; then - echo "export PATH=\"\$PATH:$path_dir\"" >> "$profile" - echo "Added $path_dir to $profile" - fi - fi - done - - local fish_config="$HOME/.config/fish/config.fish" - if [ -f "$fish_config" ]; then - if ! grep -q "$path_dir" "$fish_config" 2> /dev/null; then - echo "fish_add_path $path_dir" >> "$fish_config" - echo "Added $path_dir to $fish_config" - fi - fi -} - -function ensure_claude_in_path() { - 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 [ -z "$CLAUDE_BIN" ] || [ ! -x "$CLAUDE_BIN" ]; then - echo "Warning: Could not find claude binary" - return - fi - - local CLAUDE_DIR - CLAUDE_DIR=$(dirname "$CLAUDE_BIN") - - if [ -n "${CODER_SCRIPT_BIN_DIR:-}" ] && [ ! -e "$CODER_SCRIPT_BIN_DIR/claude" ]; then - ln -s "$CLAUDE_BIN" "$CODER_SCRIPT_BIN_DIR/claude" - echo "Created symlink: $CODER_SCRIPT_BIN_DIR/claude -> $CLAUDE_BIN" - fi - - add_path_to_shell_profiles "$CLAUDE_DIR" -} - -function install_claude_code_cli() { - if [ "$ARG_INSTALL_CLAUDE_CODE" != "true" ]; then - echo "Skipping Claude Code installation as per configuration." - ensure_claude_in_path - return - fi - - 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" - fi - echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')" - - ensure_claude_in_path -} - -function setup_claude_configurations() { - if [ ! -d "$ARG_WORKDIR" ]; then - echo "Warning: The specified folder '$ARG_WORKDIR' does not exist." - echo "Creating the folder..." - mkdir -p "$ARG_WORKDIR" - echo "Folder created successfully." - fi - - module_path="$HOME/.coder-modules/coder/claude-code" - mkdir -p "$module_path" - - if [ "$ARG_MCP" != "" ]; then - ( - cd "$ARG_WORKDIR" - add_mcp_servers "$ARG_MCP" "in $ARG_WORKDIR" - ) - fi - - if [ -n "$ARG_MCP_CONFIG_REMOTE_PATH" ] && [ "$ARG_MCP_CONFIG_REMOTE_PATH" != "[]" ]; then - ( - cd "$ARG_WORKDIR" - for url in $(echo "$ARG_MCP_CONFIG_REMOTE_PATH" | jq -r '.[]'); do - echo "Fetching MCP configuration from: $url" - mcp_json=$(curl -fsSL "$url") || { - echo "Warning: Failed to fetch MCP configuration from '$url', continuing..." - continue - } - if ! echo "$mcp_json" | jq -e '.mcpServers' > /dev/null 2>&1; then - echo "Warning: Invalid MCP configuration from '$url' (missing mcpServers), continuing..." - continue - fi - add_mcp_servers "$mcp_json" "from $url" - done - ) - fi - -} - -function configure_standalone_mode() { - echo "Configuring Claude Code for standalone mode..." - - if [ -z "${ANTHROPIC_API_KEY:-}" ] && [ -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ] && [ "$ARG_ENABLE_AI_GATEWAY" = "false" ]; then - echo "Note: No authentication configured (anthropic_api_key, claude_code_oauth_token, enable_ai_gateway), skipping onboarding bypass" - return - fi - - local claude_config="$HOME/.claude.json" - - # Create or update .claude.json with minimal configuration for API key auth - # This skips the interactive login prompt and onboarding screens - if [ -f "$claude_config" ]; then - echo "Updating existing Claude configuration at $claude_config" - - jq --arg workdir "$ARG_WORKDIR" --arg apikey "${ANTHROPIC_API_KEY:-}" \ - '.autoUpdaterStatus = "disabled" | - .autoModeAccepted = true | - .bypassPermissionsModeAccepted = true | - .hasAcknowledgedCostThreshold = true | - .hasCompletedOnboarding = true | - .primaryApiKey = $apikey | - .projects[$workdir].hasCompletedProjectOnboarding = true | - .projects[$workdir].hasTrustDialogAccepted = true' \ - "$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", - "autoModeAccepted": true, - "bypassPermissionsModeAccepted": true, - "hasAcknowledgedCostThreshold": true, - "hasCompletedOnboarding": true, - "primaryApiKey": "${ANTHROPIC_API_KEY:-}", - "projects": { - "$ARG_WORKDIR": { - "hasCompletedProjectOnboarding": true, - "hasTrustDialogAccepted": true - } - } -} -EOF - fi - - echo "Standalone mode configured successfully" -} - -install_claude_code_cli -setup_claude_configurations -configure_standalone_mode diff --git a/registry/coder/modules/claude-code/scripts/install.sh.tftpl b/registry/coder/modules/claude-code/scripts/install.sh.tftpl new file mode 100644 index 000000000..82f741b07 --- /dev/null +++ b/registry/coder/modules/claude-code/scripts/install.sh.tftpl @@ -0,0 +1,201 @@ +#!/bin/bash + +set -euo pipefail + +BOLD='\033[0;1m' + +command_exists() { + command -v "$1" > /dev/null 2>&1 +} + +ARG_CLAUDE_CODE_VERSION='${ARG_CLAUDE_CODE_VERSION}' +ARG_WORKDIR='${ARG_WORKDIR}' +ARG_INSTALL_CLAUDE_CODE='${ARG_INSTALL_CLAUDE_CODE}' +ARG_CLAUDE_BINARY_PATH='${ARG_CLAUDE_BINARY_PATH}' +ARG_CLAUDE_BINARY_PATH="$${ARG_CLAUDE_BINARY_PATH/#\~/$HOME}" +ARG_CLAUDE_BINARY_PATH="$${ARG_CLAUDE_BINARY_PATH//\$HOME/$HOME}" +ARG_MCP=$(echo -n '${ARG_MCP}' | base64 -d) +ARG_MCP_CONFIG_REMOTE_PATH=$(echo -n '${ARG_MCP_CONFIG_REMOTE_PATH}' | base64 -d) +ARG_ENABLE_AI_GATEWAY='${ARG_ENABLE_AI_GATEWAY}' + +export PATH="$${ARG_CLAUDE_BINARY_PATH}:$PATH" + +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_MCP: %s\n" "$${ARG_MCP}" +printf "ARG_MCP_CONFIG_REMOTE_PATH: %s\n" "$${ARG_MCP_CONFIG_REMOTE_PATH}" +printf "ARG_ENABLE_AI_GATEWAY: %s\n" "$${ARG_ENABLE_AI_GATEWAY}" + +echo "--------------------------------" + +function add_mcp_servers() { + local mcp_json="$1" + local source_desc="$2" + + while IFS= read -r server_name && IFS= read -r server_json; do + echo "------------------------" + echo "Executing: claude mcp add-json \"$${server_name}\" '$${server_json}' ($${source_desc})" + claude mcp add-json "$${server_name}" "$${server_json}" || echo "Warning: Failed to add MCP server '$${server_name}', continuing..." + echo "------------------------" + echo "" + done < <(echo "$${mcp_json}" | jq -r '.mcpServers | to_entries[] | .key, (.value | @json)') +} + +function add_path_to_shell_profiles() { + local path_dir="$1" + + for profile in "$HOME/.profile" "$HOME/.bash_profile" "$HOME/.bashrc" "$HOME/.zprofile" "$HOME/.zshrc"; do + if [ -f "$${profile}" ]; then + if ! grep -q "$${path_dir}" "$${profile}" 2> /dev/null; then + echo "export PATH=\"\$PATH:$${path_dir}\"" >> "$${profile}" + echo "Added $${path_dir} to $${profile}" + fi + fi + done + + local fish_config="$HOME/.config/fish/config.fish" + if [ -f "$${fish_config}" ]; then + if ! grep -q "$${path_dir}" "$${fish_config}" 2> /dev/null; then + echo "fish_add_path $${path_dir}" >> "$${fish_config}" + echo "Added $${path_dir} to $${fish_config}" + fi + fi +} + +function ensure_claude_in_path() { + 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 [ -z "$${CLAUDE_BIN}" ] || [ ! -x "$${CLAUDE_BIN}" ]; then + echo "Warning: Could not find claude binary" + return + fi + + local CLAUDE_DIR + CLAUDE_DIR=$(dirname "$${CLAUDE_BIN}") + + if [ -n "$${CODER_SCRIPT_BIN_DIR:-}" ] && [ ! -e "$${CODER_SCRIPT_BIN_DIR}/claude" ]; then + ln -s "$${CLAUDE_BIN}" "$${CODER_SCRIPT_BIN_DIR}/claude" + echo "Created symlink: $${CODER_SCRIPT_BIN_DIR}/claude -> $${CLAUDE_BIN}" + fi + + add_path_to_shell_profiles "$${CLAUDE_DIR}" +} + +function install_claude_code_cli() { + if [ "$${ARG_INSTALL_CLAUDE_CODE}" != "true" ]; then + echo "Skipping Claude Code installation as per configuration." + ensure_claude_in_path + return + fi + + 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}" + fi + echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')" + + ensure_claude_in_path +} + +function setup_claude_configurations() { + if [ ! -d "$${ARG_WORKDIR}" ]; then + echo "Warning: The specified folder '$${ARG_WORKDIR}' does not exist." + echo "Creating the folder..." + mkdir -p "$${ARG_WORKDIR}" + echo "Folder created successfully." + fi + + module_path="$HOME/.coder-modules/coder/claude-code" + mkdir -p "$${module_path}" + + if [ "$${ARG_MCP}" != "" ]; then + ( + cd "$${ARG_WORKDIR}" + add_mcp_servers "$${ARG_MCP}" "in $${ARG_WORKDIR}" + ) + fi + + if [ -n "$${ARG_MCP_CONFIG_REMOTE_PATH}" ] && [ "$${ARG_MCP_CONFIG_REMOTE_PATH}" != "[]" ]; then + ( + cd "$${ARG_WORKDIR}" + for url in $(echo "$${ARG_MCP_CONFIG_REMOTE_PATH}" | jq -r '.[]'); do + echo "Fetching MCP configuration from: $${url}" + mcp_json=$(curl -fsSL "$${url}") || { + echo "Warning: Failed to fetch MCP configuration from '$${url}', continuing..." + continue + } + if ! echo "$${mcp_json}" | jq -e '.mcpServers' > /dev/null 2>&1; then + echo "Warning: Invalid MCP configuration from '$${url}' (missing mcpServers), continuing..." + continue + fi + add_mcp_servers "$${mcp_json}" "from $${url}" + done + ) + fi + +} + +function configure_standalone_mode() { + echo "Configuring Claude Code for standalone mode..." + + if [ -z "$${ANTHROPIC_API_KEY:-}" ] && [ -z "$${CLAUDE_CODE_OAUTH_TOKEN:-}" ] && [ "$${ARG_ENABLE_AI_GATEWAY}" = "false" ]; then + echo "Note: No authentication configured (anthropic_api_key, claude_code_oauth_token, enable_ai_gateway), skipping onboarding bypass" + return + fi + + local claude_config="$HOME/.claude.json" + + if [ -f "$${claude_config}" ]; then + echo "Updating existing Claude configuration at $${claude_config}" + + jq --arg workdir "$${ARG_WORKDIR}" --arg apikey "$${ANTHROPIC_API_KEY:-}" \ + '.autoUpdaterStatus = "disabled" | + .autoModeAccepted = true | + .bypassPermissionsModeAccepted = true | + .hasAcknowledgedCostThreshold = true | + .hasCompletedOnboarding = true | + .primaryApiKey = $apikey | + .projects[$workdir].hasCompletedProjectOnboarding = true | + .projects[$workdir].hasTrustDialogAccepted = true' \ + "$${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", + "autoModeAccepted": true, + "bypassPermissionsModeAccepted": true, + "hasAcknowledgedCostThreshold": true, + "hasCompletedOnboarding": true, + "primaryApiKey": "$${ANTHROPIC_API_KEY:-}", + "projects": { + "$${ARG_WORKDIR}": { + "hasCompletedProjectOnboarding": true, + "hasTrustDialogAccepted": true + } + } +} +EOF + fi + + echo "Standalone mode configured successfully" +} + +install_claude_code_cli +setup_claude_configurations +configure_standalone_mode From 46ce616994a919e27f8561b7936725568ff9edad Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 24 Apr 2026 08:35:30 +0000 Subject: [PATCH 3/4] refactor(registry/coder/modules/claude-code): make workdir optional and scope MCP to user workdir is now optional. When set, the module still pre-creates the directory and pre-accepts the Claude Code trust dialog for it. When unset, the module installs the CLI and configures authentication only; users accept trust dialogs interactively per project. MCP servers are added at Claude Code's user scope via `claude mcp add-json --scope user` so they are available across every project the workspace owner opens, instead of being tied to a single project directory. For project-local MCP servers, callers should commit a `.mcp.json` to the project repository rather than passing it through this module. Drop primaryApiKey from the standalone-mode config writer. Claude Code reads credentials from the ANTHROPIC_API_KEY and CLAUDE_CODE_OAUTH_TOKEN env vars (which the module already exports via coder_env); writing the key into ~/.claude.json had no effect on authentication. Split the standalone-mode .claude.json writer into two steps: the always-on auth/onboarding keys, and the optional `.projects[workdir]` trust block that only runs when workdir is set. --- registry/coder/modules/claude-code/README.md | 9 ++- .../coder/modules/claude-code/main.test.ts | 10 +-- registry/coder/modules/claude-code/main.tf | 9 +-- .../coder/modules/claude-code/main.tftest.hcl | 13 ++++ .../claude-code/scripts/install.sh.tftpl | 63 ++++++++----------- 5 files changed, 58 insertions(+), 46 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 0a082df15..cab5ed332 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -15,11 +15,18 @@ module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" version = "5.0.0" agent_id = coder_agent.main.id - workdir = "/home/coder/project" anthropic_api_key = "xxxx-xxxxx-xxxx" } ``` +## workdir + +`workdir` is optional. When set, the module pre-creates the directory if it is missing and pre-accepts the Claude Code trust/onboarding prompt for it in `~/.claude.json`. Leave `workdir` unset if you only want the module to install the CLI and configure authentication; users can still open any project interactively and accept the trust dialog per project. + +## MCP scope + +Servers configured through `mcp` or `mcp_config_remote_path` are added at Claude Code's [user scope](https://docs.claude.com/en/docs/claude-code/mcp#scope), which makes them available across every project the workspace owner opens. For project-local MCP servers, commit a `.mcp.json` file to the project repository instead of passing it through this module. + ## Prerequisites Provide exactly one authentication method: diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index 486f7a997..6b1367551 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -343,10 +343,10 @@ describe("claude-code", async () => { // Should contain the MCP server add command from the successful fetch. expect(installLog).toContain( - "Added stdio MCP server go-language-server to local config", + "Added stdio MCP server go-language-server to user config", ); expect(installLog).toContain( - "Added stdio MCP server typescript-language-server to local config", + "Added stdio MCP server typescript-language-server to user config", ); // Verify the MCP config was added to .claude.json. @@ -380,7 +380,6 @@ describe("claude-code", async () => { "/home/coder/.claude.json", ); const parsed = JSON.parse(claudeConfig); - expect(parsed.primaryApiKey).toBe(apiKey); expect(parsed.autoUpdaterStatus).toBe("disabled"); expect(parsed.hasCompletedOnboarding).toBe(true); expect(parsed.bypassPermissionsModeAccepted).toBe(true); @@ -405,8 +404,9 @@ describe("claude-code", async () => { expect(installLog).toContain("Standalone mode configured successfully"); expect(installLog).not.toContain("skipping onboarding bypass"); - // Onboarding bypass flags must be present; primaryApiKey is unused when - // auth happens via CLAUDE_CODE_OAUTH_TOKEN. + // Onboarding bypass flags must be present. Authentication happens via + // the ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN env vars, not via + // .claude.json. const claudeConfig = await readFileContainer( id, "/home/coder/.claude.json", diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index debb6f1b8..346930db9 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -26,7 +26,8 @@ variable "icon" { variable "workdir" { type = string - description = "Project directory to pre-configure for Claude Code. The module creates this directory if it is missing, registers MCP servers against it, and pre-accepts the trust/onboarding prompts for it in ~/.claude.json." + description = "Optional project directory. When set, the module pre-creates it if missing and pre-accepts the Claude Code trust/onboarding prompt for it in ~/.claude.json." + default = null } variable "pre_install_script" { @@ -73,13 +74,13 @@ variable "model" { variable "mcp" { type = string - description = "JSON-encoded string to configure MCP servers for Claude Code. When set, writes MCP configuration into the Claude Code local scope." + description = "JSON-encoded string of MCP server configurations. When set, servers are added at Claude Code's user scope so they are available across every project the workspace owner opens." default = "" } variable "mcp_config_remote_path" { type = list(string) - description = "List of URLs that return JSON MCP server configurations (text/plain with valid JSON)" + description = "List of URLs that return JSON MCP server configurations (text/plain with valid JSON). Servers are added at Claude Code's user scope." default = [] } @@ -163,7 +164,7 @@ resource "coder_env" "anthropic_base_url" { } locals { - workdir = trimsuffix(var.workdir, "/") + workdir = var.workdir != null ? trimsuffix(var.workdir, "/") : "" install_script = templatefile("${path.module}/scripts/install.sh.tftpl", { ARG_CLAUDE_CODE_VERSION = var.claude_code_version ARG_INSTALL_CLAUDE_CODE = tostring(var.install_claude_code) diff --git a/registry/coder/modules/claude-code/main.tftest.hcl b/registry/coder/modules/claude-code/main.tftest.hcl index 071810c73..08e0c005b 100644 --- a/registry/coder/modules/claude-code/main.tftest.hcl +++ b/registry/coder/modules/claude-code/main.tftest.hcl @@ -270,3 +270,16 @@ run "test_script_outputs_with_pre_and_post" { error_message = "scripts output should list pre_install, install, post_install in run order" } } + +run "test_workdir_optional" { + command = plan + + variables { + agent_id = "test-agent-no-workdir" + } + + assert { + condition = var.workdir == null + error_message = "workdir should default to null when omitted" + } +} diff --git a/registry/coder/modules/claude-code/scripts/install.sh.tftpl b/registry/coder/modules/claude-code/scripts/install.sh.tftpl index 82f741b07..bd142c5d3 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh.tftpl +++ b/registry/coder/modules/claude-code/scripts/install.sh.tftpl @@ -38,8 +38,8 @@ function add_mcp_servers() { while IFS= read -r server_name && IFS= read -r server_json; do echo "------------------------" - echo "Executing: claude mcp add-json \"$${server_name}\" '$${server_json}' ($${source_desc})" - claude mcp add-json "$${server_name}" "$${server_json}" || echo "Warning: Failed to add MCP server '$${server_name}', continuing..." + echo "Executing: claude mcp add-json --scope user \"$${server_name}\" '$${server_json}' ($${source_desc})" + claude mcp add-json --scope user "$${server_name}" "$${server_json}" || echo "Warning: Failed to add MCP server '$${server_name}', continuing..." echo "------------------------" echo "" done < <(echo "$${mcp_json}" | jq -r '.mcpServers | to_entries[] | .key, (.value | @json)') @@ -113,7 +113,7 @@ function install_claude_code_cli() { } function setup_claude_configurations() { - if [ ! -d "$${ARG_WORKDIR}" ]; then + if [ -n "$${ARG_WORKDIR}" ] && [ ! -d "$${ARG_WORKDIR}" ]; then echo "Warning: The specified folder '$${ARG_WORKDIR}' does not exist." echo "Creating the folder..." mkdir -p "$${ARG_WORKDIR}" @@ -124,28 +124,22 @@ function setup_claude_configurations() { mkdir -p "$${module_path}" if [ "$${ARG_MCP}" != "" ]; then - ( - cd "$${ARG_WORKDIR}" - add_mcp_servers "$${ARG_MCP}" "in $${ARG_WORKDIR}" - ) + add_mcp_servers "$${ARG_MCP}" "from module input" fi if [ -n "$${ARG_MCP_CONFIG_REMOTE_PATH}" ] && [ "$${ARG_MCP_CONFIG_REMOTE_PATH}" != "[]" ]; then - ( - cd "$${ARG_WORKDIR}" - for url in $(echo "$${ARG_MCP_CONFIG_REMOTE_PATH}" | jq -r '.[]'); do - echo "Fetching MCP configuration from: $${url}" - mcp_json=$(curl -fsSL "$${url}") || { - echo "Warning: Failed to fetch MCP configuration from '$${url}', continuing..." - continue - } - if ! echo "$${mcp_json}" | jq -e '.mcpServers' > /dev/null 2>&1; then - echo "Warning: Invalid MCP configuration from '$${url}' (missing mcpServers), continuing..." - continue - fi - add_mcp_servers "$${mcp_json}" "from $${url}" - done - ) + for url in $(echo "$${ARG_MCP_CONFIG_REMOTE_PATH}" | jq -r '.[]'); do + echo "Fetching MCP configuration from: $${url}" + mcp_json=$(curl -fsSL "$${url}") || { + echo "Warning: Failed to fetch MCP configuration from '$${url}', continuing..." + continue + } + if ! echo "$${mcp_json}" | jq -e '.mcpServers' > /dev/null 2>&1; then + echo "Warning: Invalid MCP configuration from '$${url}' (missing mcpServers), continuing..." + continue + fi + add_mcp_servers "$${mcp_json}" "from $${url}" + done fi } @@ -163,15 +157,11 @@ function configure_standalone_mode() { if [ -f "$${claude_config}" ]; then echo "Updating existing Claude configuration at $${claude_config}" - jq --arg workdir "$${ARG_WORKDIR}" --arg apikey "$${ANTHROPIC_API_KEY:-}" \ - '.autoUpdaterStatus = "disabled" | + jq '.autoUpdaterStatus = "disabled" | .autoModeAccepted = true | .bypassPermissionsModeAccepted = true | .hasAcknowledgedCostThreshold = true | - .hasCompletedOnboarding = true | - .primaryApiKey = $apikey | - .projects[$workdir].hasCompletedProjectOnboarding = true | - .projects[$workdir].hasTrustDialogAccepted = true' \ + .hasCompletedOnboarding = true' \ "$${claude_config}" > "$${claude_config}.tmp" && mv "$${claude_config}.tmp" "$${claude_config}" else echo "Creating new Claude configuration at $${claude_config}" @@ -181,18 +171,19 @@ function configure_standalone_mode() { "autoModeAccepted": true, "bypassPermissionsModeAccepted": true, "hasAcknowledgedCostThreshold": true, - "hasCompletedOnboarding": true, - "primaryApiKey": "$${ANTHROPIC_API_KEY:-}", - "projects": { - "$${ARG_WORKDIR}": { - "hasCompletedProjectOnboarding": true, - "hasTrustDialogAccepted": true - } - } + "hasCompletedOnboarding": true } EOF fi + if [ -n "$${ARG_WORKDIR}" ]; then + echo "Pre-accepting trust dialog for $${ARG_WORKDIR}" + jq --arg workdir "$${ARG_WORKDIR}" \ + '.projects[$workdir].hasCompletedProjectOnboarding = true | + .projects[$workdir].hasTrustDialogAccepted = true' \ + "$${claude_config}" > "$${claude_config}.tmp" && mv "$${claude_config}.tmp" "$${claude_config}" + fi + echo "Standalone mode configured successfully" } From 297d7369cc68d27809b7ffd6f462fc5ef30116b6 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Fri, 24 Apr 2026 20:50:58 +0500 Subject: [PATCH 4/4] docs(registry/coder/modules/claude-code): reorder README and add v4 migration warning --- registry/coder/modules/claude-code/README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index cab5ed332..c51b8f8f0 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -19,13 +19,8 @@ module "claude-code" { } ``` -## workdir - -`workdir` is optional. When set, the module pre-creates the directory if it is missing and pre-accepts the Claude Code trust/onboarding prompt for it in `~/.claude.json`. Leave `workdir` unset if you only want the module to install the CLI and configure authentication; users can still open any project interactively and accept the trust dialog per project. - -## MCP scope - -Servers configured through `mcp` or `mcp_config_remote_path` are added at Claude Code's [user scope](https://docs.claude.com/en/docs/claude-code/mcp#scope), which makes them available across every project the workspace owner opens. For project-local MCP servers, commit a `.mcp.json` file to the project repository instead of passing it through this module. +> [!WARNING] +> If upgrading from v4.x.x of this module: v5 is a major refactor that drops support for [Coder Tasks](https://coder.com/docs/ai-coder/tasks) and [Boundary](https://coder.com/docs/ai-coder/agent-firewall). We plan to add those back in a follow-up. Keep using v4.x.x if you depend on them. See [#861](https://github.com/coder/registry/pulls/861) for the full migration guide. ## Prerequisites @@ -35,6 +30,10 @@ Provide exactly one authentication method: - **Claude.ai OAuth token** (Pro, Max, or Enterprise accounts): generate one by running `claude setup-token` locally and pass it as `claude_code_oauth_token`. - **Coder AI Gateway** (Coder Premium, Coder >= 2.30.0): set `enable_ai_gateway = true`. The module authenticates against the gateway using the workspace owner's session token. Do not combine with `anthropic_api_key` or `claude_code_oauth_token`. +## workdir + +`workdir` is optional. When set, the module pre-creates the directory if it is missing and pre-accepts the Claude Code trust/onboarding prompt for it in `~/.claude.json`. Leave `workdir` unset if you only want the module to install the CLI and configure authentication; users can still open any project interactively and accept the trust dialog per project. + ## Examples ### Standalone mode with a launcher app @@ -139,6 +138,9 @@ module "claude-code" { > [!NOTE] > Swap `anthropic_api_key` for `claude_code_oauth_token = "xxxxx-xxxx-xxxx"` to authenticate via a Claude.ai OAuth token instead. Pass exactly one. +> [!NOTE] +> Servers configured through `mcp` or `mcp_config_remote_path` are added at Claude Code's [user scope](https://docs.claude.com/en/docs/claude-code/mcp#scope), making them available across every project the workspace owner opens. For project-local MCP servers, commit a `.mcp.json` to the project repository instead. + > [!NOTE] > Remote URLs should return a JSON body in the following format: >