Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
eb2da40
feat: enhance boundary configuration options in Claude Code module
DevelopmentCats Mar 11, 2026
9578f81
chore: update Claude Code module version to 4.9.0 in README
DevelopmentCats Mar 11, 2026
fe3cf30
refactor: button up boundary related testing
DevelopmentCats Mar 12, 2026
352b775
feat: address comments from Copilot and tighten up config behaviour a…
DevelopmentCats Mar 12, 2026
5f83d81
fix: improve boundary configuration validation in start script
DevelopmentCats Mar 12, 2026
738c972
fix: add boundary config validation and path normalization
DevelopmentCats Apr 4, 2026
20993b0
fix: align version attribute in tf code blocks for Prettier formatting
DevelopmentCats Apr 4, 2026
8f68535
fix: address review comments on boundary config validation and safety…
DevelopmentCats Apr 4, 2026
022fd1c
fix(claude-code): expand tilde and \$HOME in boundary config path bef…
DevelopmentCats Apr 5, 2026
aaf149f
fix: address all Copilot review comments on boundary config validation
Apr 5, 2026
17f9319
fix: restore version attribute alignment in README tf code blocks
DevelopmentCats Apr 7, 2026
b82fc8c
fix(claude-code): remove redundant ARG_BOUNDARY_CONFIG_PATH normaliza…
DevelopmentCats Apr 8, 2026
890f649
chore: resolve merge conflict with main, keeping all boundary tests
DevelopmentCats Apr 8, 2026
3d16037
chore(claude-code): bump version to 4.9.3 after addressing review com…
DevelopmentCats Apr 9, 2026
132eea2
fix(claude-code): align version attribute in README code examples
DevelopmentCats Apr 9, 2026
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.9.2"
version = "4.9.3"
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.9.2"
version = "4.9.3"
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.3"
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.9.2"
version = "4.9.3"
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.9.2"
version = "4.9.3"
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.9.2"
version = "4.9.3"
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.9.2"
version = "4.9.3"
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.9.2"
version = "4.9.3"
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.9.2"
version = "4.9.3"
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.9.2"
version = "4.9.3"
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 @@ -231,6 +231,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 || trimspace(var.boundary_config) == "") || (var.boundary_config_path == null || trimspace(var.boundary_config_path) == "")
error_message = "Only one of boundary_config or boundary_config_path can be provided, not both."
}

validation {
condition = ((var.boundary_config == null || trimspace(var.boundary_config) == "") && (var.boundary_config_path == null || trimspace(var.boundary_config_path) == "")) || 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 @@ -331,8 +363,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 && trimspace(var.boundary_config) != "" ? 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 @@ -407,6 +440,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 && trimspace(var.boundary_config_path) != "" ? trimspace(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,6 +188,74 @@ run "test_claude_code_permission_mode_validation" {
}
}

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 {
condition = var.enable_boundary == true
error_message = "Boundary should be enabled"
}

assert {
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_auto_permission_mode" {
command = plan

Expand Down Expand Up @@ -216,11 +284,112 @@ run "test_claude_code_with_boundary" {
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_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