Files
registry/registry/coder/modules/claude-code/main.tf
T
35C4n0r 9e89f04691 fix: session resumption fix, and bug fixes for arg path logic (#522)
## Description

Fix issue with commands being injected through prompt.

Bug fix for logic in arg paths.
<!-- Briefly describe what this PR does and why -->

## Type of Change

- [ ] New module
- [ ] New template
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/claude-code`  
**New version:** `v4.0.1`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun fmt`)
- [ ] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->

---------

Co-authored-by: DevelopmentCats <christofer@coder.com>
Co-authored-by: DevelopmentCats <chris@dualriver.com>
2025-11-12 09:33:37 -06:00

386 lines
12 KiB
Terraform
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.12"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
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."
default = "/icon/claude.svg"
}
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 "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"
}
variable "pre_install_script" {
type = string
description = "Custom script to run before installing Claude Code."
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after installing Claude Code."
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.10.0"
}
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."
default = true
}
variable "claude_code_version" {
type = string
description = "The version of Claude Code to install."
default = "latest"
}
variable "claude_api_key" {
type = string
description = "The API key to use for the Claude Code server."
default = ""
}
variable "model" {
type = string
description = "Sets the model for the current session with an alias for the latest model (sonnet or opus) or a models full name."
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", "bypassPermissions"], var.permission_mode)
error_message = "interaction_mode must be one of: default, acceptEdits, plan, bypassPermissions."
}
}
variable "mcp" {
type = string
description = "MCP JSON to be added to the claude code local scope"
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"
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 "enable_boundary" {
type = bool
description = "Whether to enable coder boundary for network filtering"
default = false
}
variable "boundary_version" {
type = string
description = "Boundary version, valid git reference should be provided (tag, commit, branch)"
default = "main"
}
variable "boundary_log_dir" {
type = string
description = "Directory for boundary logs"
default = "/tmp/boundary_logs"
}
variable "boundary_log_level" {
type = string
description = "Log level for boundary process"
default = "WARN"
}
variable "boundary_additional_allowed_urls" {
type = list(string)
description = "Additional URLs to allow through boundary (in addition to default allowed URLs)"
default = []
}
variable "boundary_proxy_port" {
type = string
description = "Port for HTTP Proxy used by Boundary"
default = "8087"
}
variable "enable_boundary_pprof" {
type = bool
description = "Whether to enable coder boundary pprof server"
default = false
}
variable "boundary_pprof_port" {
type = string
description = "Port for pprof server used by Boundary"
default = "6067"
}
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" {
agent_id = var.agent_id
name = "CLAUDE_CODE_OAUTH_TOKEN"
value = var.claude_code_oauth_token
}
resource "coder_env" "claude_api_key" {
count = length(var.claude_api_key) > 0 ? 1 : 0
agent_id = var.agent_id
name = "CLAUDE_API_KEY"
value = var.claude_api_key
}
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"
remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh"))
# Extract hostname from access_url for boundary --allow flag
coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "")
# 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("<system>%s%s</system>",
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 "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "2.0.0"
agent_id = var.agent_id
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
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
echo -n "${local.remove_last_session_id_script_b64}" | base64 -d > "/tmp/remove-last-session-id.sh"
chmod +x /tmp/start.sh
chmod +x /tmp/remove-last-session-id.sh
ARG_MODEL='${var.model}' \
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_BOUNDARY_LOG_DIR='${var.boundary_log_dir}' \
ARG_BOUNDARY_LOG_LEVEL='${var.boundary_log_level}' \
ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS='${join("|", var.boundary_additional_allowed_urls)}' \
ARG_BOUNDARY_PROXY_PORT='${var.boundary_proxy_port}' \
ARG_ENABLE_BOUNDARY_PPROF='${var.enable_boundary_pprof}' \
ARG_BOUNDARY_PPROF_PORT='${var.boundary_pprof_port}' \
ARG_CODER_HOST='${local.coder_host}' \
/tmp/start.sh
EOT
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_MCP_APP_STATUS_SLUG='${local.app_slug}' \
ARG_INSTALL_CLAUDE_CODE='${var.install_claude_code}' \
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, "'", "'\\''")) : ""}' \
/tmp/install.sh
EOT
}
output "task_app_id" {
value = module.agentapi.task_app_id
}