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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions registry/coder/modules/claude-code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
version = "4.9.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
claude_api_key = "xxxx-xxxxx-xxxx"
Expand Down Expand Up @@ -55,15 +55,43 @@ module "claude-code" {

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.
When `enable_boundary = true`, you must provide network filtering rules via one of two options:

- `boundary_config` — inline YAML string (config lives in the template)
- `boundary_config_path` — path to a config file already on disk

The module writes the config to `~/.config/coder_boundary/config.yaml` automatically.

#### Inline boundary config

```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
version = "4.9.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
enable_boundary = true

boundary_config = <<-EOT
allow:
- "*.anthropic.com"
- "*.github.com"
EOT
}
```

#### Boundary config from file path

Use this when the config file is provisioned separately or managed outside the template:

```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.9.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
enable_boundary = true
boundary_config_path = "/home/coder/.config/coder_boundary/config.yaml"
}
```

Expand All @@ -81,7 +109,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
version = "4.9.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
enable_aibridge = true
Expand Down Expand Up @@ -110,7 +138,7 @@ data "coder_task" "me" {}

module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
version = "4.9.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
ai_prompt = data.coder_task.me.prompt
Expand All @@ -133,7 +161,7 @@ This example shows additional configuration options for version pinning, custom
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
version = "4.9.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"

Expand Down Expand Up @@ -189,7 +217,7 @@ Run and configure Claude Code as a standalone CLI in your workspace.
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
version = "4.9.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
install_claude_code = true
Expand All @@ -211,7 +239,7 @@ variable "claude_code_oauth_token" {

module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
version = "4.9.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
claude_code_oauth_token = var.claude_code_oauth_token
Expand Down Expand Up @@ -284,7 +312,7 @@ resource "coder_env" "bedrock_api_key" {

module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
version = "4.9.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
Expand Down Expand Up @@ -341,7 +369,7 @@ resource "coder_env" "google_application_credentials" {

module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
version = "4.9.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
model = "claude-sonnet-4@20250514"
Expand Down
39 changes: 37 additions & 2 deletions registry/coder/modules/claude-code/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,38 @@ variable "enable_boundary" {
type = bool
description = "Whether to enable coder boundary for network filtering"
default = false

validation {
condition = !var.enable_boundary || var.boundary_config == null || var.boundary_config_path == null
error_message = "Only one of boundary_config or boundary_config_path can be provided, not both."
}

validation {
condition = (var.boundary_config == null && var.boundary_config_path == null) || var.enable_boundary
error_message = "boundary_config and boundary_config_path can only be set when enable_boundary is true."
}
}

variable "boundary_config" {
type = string
description = "Inline YAML config for coder boundary network filtering rules. Written to ~/.config/coder_boundary/config.yaml before boundary starts. Mutually exclusive with boundary_config_path."
default = null

validation {
condition = var.boundary_config == null || trimspace(var.boundary_config) != ""
error_message = "boundary_config must not be empty or whitespace-only when provided."
}
}

variable "boundary_config_path" {
type = string
description = "Path to an existing boundary config file on disk. Symlinked to ~/.config/coder_boundary/config.yaml before boundary starts. Mutually exclusive with boundary_config."
default = null

validation {
condition = var.boundary_config_path == null || trimspace(var.boundary_config_path) != ""
error_message = "boundary_config_path must not be empty or whitespace-only when provided."
}
}

variable "boundary_version" {
Expand Down Expand Up @@ -325,8 +357,9 @@ locals {
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
coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "")
boundary_config_b64 = var.boundary_config != null ? base64encode(var.boundary_config) : ""
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
Expand Down Expand Up @@ -400,6 +433,8 @@ module "agentapi" {
ARG_COMPILE_FROM_SOURCE='${var.compile_boundary_from_source}' \
ARG_USE_BOUNDARY_DIRECTLY='${var.use_boundary_directly}' \
ARG_CODER_HOST='${local.coder_host}' \
ARG_BOUNDARY_CONFIG='${local.boundary_config_b64}' \
ARG_BOUNDARY_CONFIG_PATH='${var.boundary_config_path != null ? var.boundary_config_path : ""}' \
ARG_CLAUDE_BINARY_PATH='${var.claude_binary_path}' \
/tmp/start.sh
EOT
Expand Down
175 changes: 172 additions & 3 deletions registry/coder/modules/claude-code/main.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,61 @@ run "test_claude_code_permission_mode_validation" {
}
}

run "test_claude_code_with_boundary" {
run "test_claude_code_with_boundary_inline_config" {
command = plan

variables {
agent_id = "test-agent-boundary"
workdir = "/home/coder/boundary-test"
enable_boundary = true
boundary_config = <<-EOT
allow:
- "*.anthropic.com"
- "*.github.com"
EOT
}

override_data {
target = data.coder_workspace.me
values = {
access_url = "https://coder.example.com"
}
}

assert {
condition = var.enable_boundary == true
error_message = "Boundary should be enabled"
}

assert {
condition = var.boundary_config != null
error_message = "Boundary config should be set"
}

assert {
condition = local.coder_host == "coder.example.com"
error_message = "Coder host should be 'coder.example.com' after stripping https:// from access URL"
}

assert {
condition = local.boundary_config_b64 != ""
error_message = "Boundary config should be base64-encoded for the start script"
}

assert {
condition = base64decode(local.boundary_config_b64) == var.boundary_config
error_message = "Base64-encoded boundary config should decode back to the original config"
}
}

run "test_claude_code_with_boundary_config_path" {
command = plan

variables {
agent_id = "test-agent-boundary-path"
workdir = "/home/coder/boundary-test"
enable_boundary = true
boundary_config_path = "/home/coder/.config/coder_boundary/config.yaml"
}

assert {
Expand All @@ -203,9 +251,130 @@ run "test_claude_code_with_boundary" {
}

assert {
condition = local.coder_host != ""
error_message = "Coder host should be extracted from access URL"
condition = var.boundary_config_path == "/home/coder/.config/coder_boundary/config.yaml"
error_message = "Boundary config path should be set correctly"
}
}

run "test_claude_code_with_boundary_no_config" {
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"
}
}

run "test_boundary_both_configs_fails" {
command = plan

variables {
agent_id = "test-agent-boundary-both"
workdir = "/home/coder/boundary-test"
enable_boundary = true
boundary_config = "allow:\n - '*.example.com'"
boundary_config_path = "/home/coder/.config/coder_boundary/config.yaml"
}

expect_failures = [
var.enable_boundary,
]
}

run "test_boundary_config_without_boundary_fails" {
command = plan

variables {
agent_id = "test-agent-no-boundary"
workdir = "/home/coder/boundary-test"
enable_boundary = false
boundary_config = "allow:\n - '*.example.com'"
}

expect_failures = [
var.enable_boundary,
]
}

run "test_boundary_config_path_without_boundary_fails" {
command = plan

variables {
agent_id = "test-agent-no-boundary-path"
workdir = "/home/coder/boundary-test"
enable_boundary = false
boundary_config_path = "/home/coder/.config/coder_boundary/config.yaml"
}

expect_failures = [
var.enable_boundary,
]
}

run "test_boundary_empty_config_fails" {
command = plan

variables {
agent_id = "test-agent-empty-config"
workdir = "/home/coder/boundary-test"
enable_boundary = true
boundary_config = ""
}

expect_failures = [
var.boundary_config,
]
}

run "test_boundary_empty_config_path_fails" {
command = plan

variables {
agent_id = "test-agent-empty-config-path"
workdir = "/home/coder/boundary-test"
enable_boundary = true
boundary_config_path = ""
}

expect_failures = [
var.boundary_config_path,
]
}

run "test_boundary_whitespace_config_fails" {
command = plan

variables {
agent_id = "test-agent-whitespace-config"
workdir = "/home/coder/boundary-test"
enable_boundary = true
boundary_config = " "
}

expect_failures = [
var.boundary_config,
]
}

run "test_boundary_whitespace_config_path_fails" {
command = plan

variables {
agent_id = "test-agent-whitespace-config-path"
workdir = "/home/coder/boundary-test"
enable_boundary = true
boundary_config_path = " "
}

expect_failures = [
var.boundary_config_path,
]
}

run "test_claude_code_system_prompt" {
Expand Down
Loading
Loading