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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions registry/coder/modules/claude-code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "2.2.0"
version = "2.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true
Expand All @@ -26,6 +26,9 @@ module "claude-code" {
> 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

- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template
Expand Down Expand Up @@ -83,7 +86,7 @@ resource "coder_agent" "main" {
module "claude-code" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/claude-code/coder"
version = "2.2.0"
version = "2.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true
Expand All @@ -101,7 +104,7 @@ Run Claude Code as a standalone app in your workspace. This will install Claude
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "2.2.0"
version = "2.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true
Expand Down
35 changes: 34 additions & 1 deletion registry/coder/modules/claude-code/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
afterEach,
expect,
describe,
it,
setDefaultTimeout,
beforeAll,
} from "bun:test";
Expand Down Expand Up @@ -100,6 +101,7 @@ const writeAgentAPIMockControl = async ({
interface SetupProps {
skipAgentAPIMock?: boolean;
skipClaudeMock?: boolean;
extraVars?: Record<string, string>;
}

const projectDir = "/home/coder/project";
Expand All @@ -112,6 +114,7 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
install_claude_code: "false",
agentapi_version: "preview",
folder: projectDir,
...props?.extraVars,
},
});
await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]);
Expand Down Expand Up @@ -335,6 +338,36 @@ describe("claude-code", async () => {
id,
"/home/coder/agentapi-mock.log",
);
expect(agentApiStartLog).toContain("AGENTAPI_ALLOWED_HOSTS: *");
expect(agentApiStartLog).toContain("AGENTAPI_ALLOWED_HOSTS=*");
});

describe("subdomain", async () => {
it("sets AGENTAPI_CHAT_BASE_PATH when false", async () => {
const { id } = await setup();
const respModuleScript = await execModuleScript(id);
expect(respModuleScript.exitCode).toBe(0);
await expectAgentAPIStarted(id);
const agentApiStartLog = await readFileContainer(
id,
"/home/coder/agentapi-mock.log",
);
expect(agentApiStartLog).toContain(
"AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat",
);
});

it("does not set AGENTAPI_CHAT_BASE_PATH when true", async () => {
const { id } = await setup({
extraVars: { subdomain: "true" },
});
const respModuleScript = await execModuleScript(id);
expect(respModuleScript.exitCode).toBe(0);
await expectAgentAPIStarted(id);
const agentApiStartLog = await readFileContainer(
id,
"/home/coder/agentapi-mock.log",
);
expect(agentApiStartLog).toMatch(/AGENTAPI_CHAT_BASE_PATH=$/m);
});
});
});
8 changes: 4 additions & 4 deletions registry/coder/modules/claude-code/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,12 @@ variable "agentapi_version" {
variable "subdomain" {
type = bool
description = "Whether to use a subdomain for the Claude Code app."
default = true
default = false
}

locals {
# we have to trim the slash because otherwise coder exp mcp will
# set up an invalid claude config
# set up an invalid claude config
workdir = trimsuffix(var.folder, "/")
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
Expand Down Expand Up @@ -244,7 +244,7 @@ resource "coder_script" "claude_code" {

# Disable host header check since AgentAPI is proxied by Coder (which does its own validation)
export AGENTAPI_ALLOWED_HOSTS="*"

# Set chat base path for non-subdomain routing (only set if not using subdomain)
export AGENTAPI_CHAT_BASE_PATH="${local.agentapi_chat_base_path}"

Expand Down Expand Up @@ -295,4 +295,4 @@ resource "coder_ai_task" "claude_code" {
sidebar_app {
id = coder_app.claude_code_web.id
}
}
}
3 changes: 2 additions & 1 deletion registry/coder/modules/claude-code/testdata/agentapi-mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ if (

fs.writeFileSync(
"/home/coder/agentapi-mock.log",
`AGENTAPI_ALLOWED_HOSTS: ${process.env.AGENTAPI_ALLOWED_HOSTS}`,
`AGENTAPI_ALLOWED_HOSTS=${process.env.AGENTAPI_ALLOWED_HOSTS}
AGENTAPI_CHAT_BASE_PATH=${process.env.AGENTAPI_CHAT_BASE_PATH}`,
);

console.log(`starting server on port ${port}`);
Expand Down