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
34 changes: 27 additions & 7 deletions registry/coder/modules/claude-code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Install and configure the [Claude Code](https://docs.anthropic.com/en/docs/agent
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "5.0.0"
version = "5.1.0"
agent_id = coder_agent.main.id
anthropic_api_key = "xxxx-xxxxx-xxxx"
}
Expand Down Expand Up @@ -47,7 +47,7 @@ locals {

module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "5.0.0"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = local.claude_workdir
anthropic_api_key = "xxxx-xxxxx-xxxx"
Expand Down Expand Up @@ -78,7 +78,7 @@ resource "coder_app" "claude" {
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "5.0.0"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
enable_ai_gateway = true
Expand All @@ -102,7 +102,7 @@ This example shows version pinning, a pre-installed binary path, a custom model,
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "5.0.0"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"

Expand Down Expand Up @@ -166,7 +166,7 @@ Downstream `coder_script` resources can wait for this module's install pipeline
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "5.0.0"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
anthropic_api_key = "xxxx-xxxxx-xxxx"
Expand All @@ -189,6 +189,26 @@ resource "coder_script" "post_claude" {
}
```

### Session lifecycle

The module writes a small managed-settings drop-in to `/etc/claude-code/managed-settings.d/30-coder-lifecycle.json` that:

- registers a `Stop` hook which touches `~/.coder-modules/coder/claude-code/last-stop` whenever Claude finishes a turn. Templates can read that file's modification time to drive workspace autostop or activity tracking.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we elaborate a bit more on how templates can use this for activity tracking?

- sets `cleanupPeriodDays` when `transcript_retention_days` is provided, so Claude Code prunes session JSONL transcripts under `~/.claude/projects/` automatically. When unset, Claude Code's default retention (30 days) applies.

```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
anthropic_api_key = "xxxx-xxxxx-xxxx"
transcript_retention_days = 7
}
```

The drop-in is a local file read by the Claude CLI at startup; it works with any inference backend (Anthropic API, Bedrock, Vertex, AI Gateway). If `/etc/claude-code` is not writable in the workspace image and `sudo` is unavailable, the install script logs a warning and skips the write.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we, in this case, write to the user's $HOME, e.g., $HOME/.claude/settings.json?


### Usage with AWS Bedrock

#### Prerequisites
Expand Down Expand Up @@ -252,7 +272,7 @@ resource "coder_env" "bedrock_api_key" {

module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "5.0.0"
version = "5.1.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 @@ -309,7 +329,7 @@ resource "coder_env" "google_application_credentials" {

module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "5.0.0"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
model = "claude-sonnet-4@20250514"
Expand Down
40 changes: 40 additions & 0 deletions registry/coder/modules/claude-code/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,4 +435,44 @@ describe("claude-code", async () => {
]);
expect(resp.stdout.trim()).toBe("ABSENT");
});

test("lifecycle-settings-written", async () => {
const { id, scripts } = await setup({
moduleVariables: {
transcript_retention_days: "7",
},
});
await runScripts(id, scripts);

const installLog = await readFileContainer(
id,
"/home/coder/.coder-modules/coder/claude-code/logs/install.log",
);
expect(installLog).toContain("Wrote Claude Code lifecycle settings to");

const policy = await readFileContainer(
id,
"/etc/claude-code/managed-settings.d/30-coder-lifecycle.json",
);
const parsed = JSON.parse(policy);
expect(parsed.cleanupPeriodDays).toBe(7);
expect(parsed.hooks.Stop[0].hooks[0].type).toBe("command");
expect(parsed.hooks.Stop[0].hooks[0].command).toContain(
"/home/coder/.coder-modules/coder/claude-code/last-stop",
);
});

test("lifecycle-settings-default-retention", async () => {
const { id, scripts } = await setup();
await runScripts(id, scripts);

const policy = await readFileContainer(
id,
"/etc/claude-code/managed-settings.d/30-coder-lifecycle.json",
);
const parsed = JSON.parse(policy);
// Stop hook is always present; cleanupPeriodDays only when explicitly set.
expect(parsed.hooks.Stop[0].hooks[0].type).toBe("command");
expect(parsed.cleanupPeriodDays).toBeUndefined();
});
});
25 changes: 18 additions & 7 deletions registry/coder/modules/claude-code/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ variable "disable_autoupdater" {
default = false
}

variable "transcript_retention_days" {
type = number
description = "Days to keep Claude Code session transcripts before automatic cleanup. Maps to Claude Code's cleanupPeriodDays setting via /etc/claude-code/managed-settings.d/. Defaults to Claude Code's built-in retention (30 days) when unset."
default = null
validation {
condition = var.transcript_retention_days == null ? true : var.transcript_retention_days >= 1
error_message = "transcript_retention_days must be at least 1."
}
}

variable "anthropic_api_key" {
type = string
description = "API key passed to Claude Code via the ANTHROPIC_API_KEY env var."
Expand Down Expand Up @@ -166,13 +176,14 @@ resource "coder_env" "anthropic_base_url" {
locals {
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)
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)
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)
ARG_TRANSCRIPT_RETENTION_DAYS = var.transcript_retention_days != null ? tostring(var.transcript_retention_days) : ""
})
module_dir_name = ".coder-modules/coder/claude-code"
}
Expand Down
33 changes: 33 additions & 0 deletions registry/coder/modules/claude-code/scripts/install.sh.tftpl
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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}'
ARG_TRANSCRIPT_RETENTION_DAYS='${ARG_TRANSCRIPT_RETENTION_DAYS}'

export PATH="$${ARG_CLAUDE_BINARY_PATH}:$PATH"

Expand All @@ -29,6 +30,7 @@ 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}"
printf "ARG_TRANSCRIPT_RETENTION_DAYS: %s\n" "$${ARG_TRANSCRIPT_RETENTION_DAYS}"

echo "--------------------------------"

Expand Down Expand Up @@ -144,6 +146,36 @@ function setup_claude_configurations() {

}

function configure_lifecycle_settings() {
local dropin_dir="/etc/claude-code/managed-settings.d"
local target="$${dropin_dir}/30-coder-lifecycle.json"
# Bake the absolute path at install time so the hook does not depend on
# $HOME being set identically when Claude Code executes it.
local sentinel="$HOME/.coder-modules/coder/claude-code/last-stop"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should depend on module_dir_name input. Although we have a very strict regex there, but hardcoding this would be less flexible for any future changes.


local payload
payload=$(jq -n \
--arg cmd "touch $${sentinel}" \
--arg retention "$${ARG_TRANSCRIPT_RETENTION_DAYS}" \
'{
hooks: { Stop: [{ hooks: [{ type: "command", command: $cmd }] }] }
} + (if $retention != "" then { cleanupPeriodDays: ($retention | tonumber) } else {} end)')

if command_exists sudo && sudo -n true 2> /dev/null; then
sudo mkdir -p "$${dropin_dir}"
echo "$${payload}" | sudo tee "$${target}" > /dev/null
sudo chmod 0644 "$${target}"
elif mkdir -p "$${dropin_dir}" 2> /dev/null; then
echo "$${payload}" > "$${target}"
chmod 0644 "$${target}"
else
echo "Warning: cannot write to $${dropin_dir} (no sudo and directory not writable); skipping lifecycle settings"
return
fi

echo "Wrote Claude Code lifecycle settings to $${target}"
}

function configure_standalone_mode() {
echo "Configuring Claude Code for standalone mode..."

Expand Down Expand Up @@ -189,4 +221,5 @@ EOF

install_claude_code_cli
setup_claude_configurations
configure_lifecycle_settings
configure_standalone_mode