From 3e2f0508e4d2c2c0ca822b23c2a0423fffa6a1fe Mon Sep 17 00:00:00 2001 From: Randy Fay Date: Sun, 26 Jul 2026 12:30:10 -0600 Subject: [PATCH 1/8] feat(freeform): add opt-in Claude Code remote control; persist Homebrew across restarts Homebrew (/home/linuxbrew) lived on the ephemeral container filesystem in all three templates, so brew upgrade/install could fail with a cross-device link error (cache and Cellar on different filesystems) and any successful upgrade was lost on the next workspace restart. Mount /home/linuxbrew as a persistent per-workspace volume (mirroring the existing dind-cache pattern) and relocate HOMEBREW_CACHE onto it, fixing this for every brew formula in all templates. On top of that, add enable_claude_code as an opt-in freeform-only feature: upgrades Claude Code at startup and runs it in a tmux session named after the workspace, attachable via a dashboard app button (Coder's native command app type -- no extra daemon). --dangerously-skip-permissions stays a separate, off-by-default toggle. Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 3 +- drupal-contrib/template.tf | 16 ++++++ drupal-contrib/tests/validate.tftest.hcl | 8 +++ drupal-core/template.tf | 16 ++++++ drupal-core/tests/validate.tftest.hcl | 11 ++++ freeform/README.md | 21 +++++++ freeform/template.tf | 71 ++++++++++++++++++++++++ freeform/tests/validate.tftest.hcl | 50 +++++++++++++++++ image/Dockerfile | 10 +++- 9 files changed, 204 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ac3d778..1d2ecfa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -233,7 +233,8 @@ The startup script is inline in each template's `template.tf` (e.g. `freeform/te ### Volume Strategy - **Home directory**: Host path `/coder-workspaces/-` → Container `/home/coder` - **Docker cache**: Named volume `coder---dind-cache` → `/var/lib/docker` -- **Isolation**: Each workspace gets separate host directory and Docker volume +- **Homebrew**: Named volume `coder---linuxbrew` → `/home/linuxbrew` — persists the Homebrew Cellar across workspace stop/start, so `brew upgrade`/`brew install` done inside a workspace survives restarts. Unlike the `/home/coder` bind mount, a fresh named volume is auto-populated by Docker from the image's existing `/home/linuxbrew` contents on first mount, so no manual copy step is needed — only an ownership fix (`sudo chown -R coder:coder /home/linuxbrew`) in the startup script. `HOMEBREW_CACHE` is set to `/home/linuxbrew/.cache/Homebrew` (via the Dockerfile) so the cache and Cellar always stay on the same filesystem, avoiding cross-device link (`EXDEV`) errors. +- **Isolation**: Each workspace gets separate host directory and Docker volumes ### Terraform Variables Key template variables (e.g. in `freeform/template.tf`): diff --git a/drupal-contrib/template.tf b/drupal-contrib/template.tf index 847dec5..be65235 100644 --- a/drupal-contrib/template.tf +++ b/drupal-contrib/template.tf @@ -313,6 +313,7 @@ resource "coder_agent" "main" { fi sudo chown coder:coder /home/coder + sudo chown -R coder:coder /home/linuxbrew if [ ! -f "/home/coder/.bashrc" ]; then echo "Initializing home directory..." @@ -1073,6 +1074,15 @@ resource "docker_volume" "coder_dind_cache" { name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}-dind-cache" } +# Persists /home/linuxbrew (Homebrew Cellar) across workspace stop/start so +# `brew upgrade`/`brew install` survives restarts. Not gated by start_count, +# matching coder_dind_cache above. Docker auto-populates a newly-created +# named volume from the image's existing directory contents on first mount, +# so no manual copy-from-image seed step is needed here. +resource "docker_volume" "coder_linuxbrew" { + name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}-linuxbrew" +} + module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-web/coder" @@ -1188,6 +1198,12 @@ resource "docker_container" "workspace" { target = "/var/lib/docker" } + mounts { + type = "volume" + source = docker_volume.coder_linuxbrew.name + target = "/home/linuxbrew" + } + env = [ "CODER_AGENT_TOKEN=${coder_agent.main.token}", "CODER_WORKSPACE_NAME=${data.coder_workspace.me.name}", diff --git a/drupal-contrib/tests/validate.tftest.hcl b/drupal-contrib/tests/validate.tftest.hcl index 609d7a9..5950625 100644 --- a/drupal-contrib/tests/validate.tftest.hcl +++ b/drupal-contrib/tests/validate.tftest.hcl @@ -78,3 +78,11 @@ run "memory_above_maximum" { } expect_failures = [var.memory] } + +run "linuxbrew_volume_created" { + command = plan + assert { + condition = docker_volume.coder_linuxbrew.name == "coder-testuser-test-workspace-linuxbrew" + error_message = "docker_volume.coder_linuxbrew must be named per the coder---linuxbrew convention" + } +} diff --git a/drupal-core/template.tf b/drupal-core/template.tf index 898991b..e2717a6 100644 --- a/drupal-core/template.tf +++ b/drupal-core/template.tf @@ -355,6 +355,7 @@ resource "coder_agent" "main" { # Since the volume comes from the host, it might have host permissions. # We fix this on every startup. sudo chown coder:coder /home/coder + sudo chown -R coder:coder /home/linuxbrew # Copy defaults if empty (first run) if [ ! -f "/home/coder/.bashrc" ]; then @@ -1362,6 +1363,15 @@ resource "docker_volume" "coder_dind_cache" { name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}-dind-cache" } +# Persists /home/linuxbrew (Homebrew Cellar) across workspace stop/start so +# `brew upgrade`/`brew install` survives restarts. Not gated by start_count, +# matching coder_dind_cache above. Docker auto-populates a newly-created +# named volume from the image's existing directory contents on first mount, +# so no manual copy-from-image seed step is needed here. +resource "docker_volume" "coder_linuxbrew" { + name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}-linuxbrew" +} + # VS Code for Web module "vscode-web" { count = data.coder_workspace.me.start_count @@ -1523,6 +1533,12 @@ resource "docker_container" "workspace" { target = "/var/lib/docker" } + mounts { + type = "volume" + source = docker_volume.coder_linuxbrew.name + target = "/home/linuxbrew" + } + # Environment variables env = [ "CODER_AGENT_TOKEN=${coder_agent.main.token}", diff --git a/drupal-core/tests/validate.tftest.hcl b/drupal-core/tests/validate.tftest.hcl index 828b0fb..27c38aa 100644 --- a/drupal-core/tests/validate.tftest.hcl +++ b/drupal-core/tests/validate.tftest.hcl @@ -82,3 +82,14 @@ run "memory_above_maximum" { } expect_failures = [var.memory] } + +run "linuxbrew_volume_created" { + command = plan + variables { + cache_path = "/tmp/mock-cache" + } + assert { + condition = docker_volume.coder_linuxbrew.name == "coder-testuser-test-workspace-linuxbrew" + error_message = "docker_volume.coder_linuxbrew must be named per the coder---linuxbrew convention" + } +} diff --git a/freeform/README.md b/freeform/README.md index 59aedbc..0fe1f13 100644 --- a/freeform/README.md +++ b/freeform/README.md @@ -89,6 +89,25 @@ ddev restart After `ddev restart`, the post-start hook updates `coder-routes.yaml` to include the Adminer router. Click **Adminer** in the Coder dashboard. +### Claude Code (Remote Control) + +Enable Claude Code remote control when creating or updating the workspace: + +```bash +coder create --template freeform myworkspace +# Select: enable_claude_code = true +``` + +At startup, Claude Code is upgraded via Homebrew and a tmux session named after the workspace is started running `claude`. Attach to it either via the **Claude Code** app button in the Coder dashboard, or manually: + +```bash +tmux attach -t myworkspace +``` + +The first attach triggers Claude Code's interactive OAuth login — no API key is configured by this template. To skip permission prompts entirely, also set `claude_code_skip_permissions = true` (off by default; use with caution). + +> **Known limitation — shared session state.** `enable_claude_code` starts exactly one tmux session per workspace, named after the workspace, running a single `claude` process. Every device/client that attaches (via the "Claude Code" app button or `tmux attach -t `) is attaching to the *same* running session — that's what makes multi-device remote control possible in the first place. The tradeoff: there's no per-client context, so `cd`-ing or `git checkout`-ing from inside that shared session changes it for everyone attached. Do routine repo administration (branch switches, directory navigation) from a separate, ordinary terminal (the Coder web terminal or `coder ssh `) instead — leave the Claude Code tmux session alone for that. + ### Other Add-Ons ```bash @@ -132,6 +151,8 @@ ddev logs -f # Follow logs | `cpu` | 4 | CPU cores (1–32) | | `memory` | 8 | Memory in GB (2–128) | | `enable_adminer` | false | Show Adminer app button (requires `ddev get ddev/ddev-adminer`) | +| `enable_claude_code` | false | Enable Claude Code remote control (tmux session + app button) | +| `claude_code_skip_permissions` | false | Append `--dangerously-skip-permissions` to the Claude Code session (requires `enable_claude_code`) | | `workspace_image_registry` | `index.docker.io/ddev/coder-ddev` | Base image registry | | `docker_gid` | 988 | Docker group GID on host | diff --git a/freeform/template.tf b/freeform/template.tf index 997b0c4..5d737fa 100644 --- a/freeform/template.tf +++ b/freeform/template.tf @@ -183,6 +183,18 @@ variable "enable_adminer" { default = false } +variable "enable_claude_code" { + description = "Enable Claude Code remote control: upgrades Claude Code via Homebrew at workspace startup and starts one persistent tmux session (named after the workspace) running `claude`, attachable from any device. Interactive OAuth login is required on first attach (no API key is configured by this template)." + type = bool + default = false +} + +variable "claude_code_skip_permissions" { + description = "Append --dangerously-skip-permissions to the `claude` command started by enable_claude_code. No effect unless enable_claude_code is also true." + type = bool + default = false +} + resource "coder_agent" "main" { arch = "amd64" os = "linux" @@ -206,6 +218,7 @@ resource "coder_agent" "main" { fi sudo chown coder:coder /home/coder + sudo chown -R coder:coder /home/linuxbrew if [ ! -f "/home/coder/.bashrc" ]; then echo "Initializing home directory..." @@ -401,6 +414,35 @@ BASHCOMP echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc fi + %{if var.enable_claude_code~} + # --- Claude Code remote control (enable_claude_code) --------------------- + echo "Setting up Claude Code remote-control session..." + + if command -v brew > /dev/null 2>&1; then + brew update > /dev/null 2>&1 || echo "Warning: brew update failed, continuing with existing formula metadata" + (brew upgrade claude-code || brew install claude-code) || \ + echo "Warning: claude-code install/upgrade via brew failed; continuing with existing version (if any)" + else + echo "Warning: brew not found on PATH; skipping claude-code upgrade" + fi + + CLAUDE_CMD="claude" + %{if var.claude_code_skip_permissions~} + CLAUDE_CMD="claude --dangerously-skip-permissions" + %{endif~} + + if command -v tmux > /dev/null 2>&1 && command -v claude > /dev/null 2>&1; then + if ! tmux has-session -t "$CODER_WORKSPACE_NAME" 2>/dev/null; then + echo "Starting Claude Code tmux session '$CODER_WORKSPACE_NAME'..." + tmux new-session -d -s "$CODER_WORKSPACE_NAME" -c "$HOME" "$CLAUDE_CMD" + else + echo "Claude Code tmux session '$CODER_WORKSPACE_NAME' already running." + fi + else + echo "Warning: tmux or claude not available; skipping Claude Code session startup" + fi + %{endif~} + echo "" echo "=== Setup Complete ===" echo "" @@ -443,6 +485,15 @@ resource "docker_volume" "coder_dind_cache" { name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}-dind-cache" } +# Persists /home/linuxbrew (Homebrew Cellar) across workspace stop/start so +# `brew upgrade`/`brew install` (e.g. for Claude Code) survives restarts. Not +# gated by start_count, matching coder_dind_cache above. Docker auto-populates +# a newly-created named volume from the image's existing directory contents +# on first mount, so no manual copy-from-image seed step is needed here. +resource "docker_volume" "coder_linuxbrew" { + name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}-linuxbrew" +} + module "vscode-web" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/vscode-web/coder" @@ -526,6 +577,20 @@ resource "coder_app" "adminer" { } } +# Claude Code remote control (enable_claude_code): opens/attaches the +# workspace's single dedicated tmux session (named after the workspace) +# running `claude`. Shared by every client that attaches — see README for +# the cd/git-checkout caveat. +resource "coder_app" "claude_code" { + for_each = var.enable_claude_code ? toset(["claude-code"]) : toset([]) + agent_id = coder_agent.main.id + slug = "claude-code" + display_name = "Claude Code" + icon = "/icon/terminal.svg" + command = "tmux attach -t $CODER_WORKSPACE_NAME || tmux new -s $CODER_WORKSPACE_NAME claude${var.claude_code_skip_permissions ? " --dangerously-skip-permissions" : ""}" + share = "owner" +} + resource "coder_script" "ddev_shutdown" { agent_id = coder_agent.main.id display_name = "Stop DDEV Projects" @@ -577,6 +642,12 @@ resource "docker_container" "workspace" { target = "/var/lib/docker" } + mounts { + type = "volume" + source = docker_volume.coder_linuxbrew.name + target = "/home/linuxbrew" + } + env = [ "CODER_AGENT_TOKEN=${coder_agent.main.token}", "CODER_WORKSPACE_NAME=${data.coder_workspace.me.name}", diff --git a/freeform/tests/validate.tftest.hcl b/freeform/tests/validate.tftest.hcl index 0edffe8..08c4cd4 100644 --- a/freeform/tests/validate.tftest.hcl +++ b/freeform/tests/validate.tftest.hcl @@ -132,3 +132,53 @@ run "memory_above_maximum" { } expect_failures = [var.memory] } + +run "linuxbrew_volume_created" { + command = plan + assert { + condition = docker_volume.coder_linuxbrew.name == "coder-testuser-test-workspace-linuxbrew" + error_message = "docker_volume.coder_linuxbrew must be named per the coder---linuxbrew convention" + } +} + +run "claude_code_off_by_default" { + command = plan + assert { + condition = length(coder_app.claude_code) == 0 + error_message = "coder_app.claude_code must not be created when enable_claude_code=false" + } +} + +run "claude_code_enabled" { + command = plan + variables { + enable_claude_code = true + } + assert { + condition = length(coder_app.claude_code) == 1 + error_message = "coder_app.claude_code must be created when enable_claude_code=true" + } +} + +run "claude_code_skip_permissions_off_by_default" { + command = plan + variables { + enable_claude_code = true + } + assert { + condition = !strcontains(coder_app.claude_code["claude-code"].command, "--dangerously-skip-permissions") + error_message = "claude_code_skip_permissions must default to false" + } +} + +run "claude_code_skip_permissions_enabled" { + command = plan + variables { + enable_claude_code = true + claude_code_skip_permissions = true + } + assert { + condition = strcontains(coder_app.claude_code["claude-code"].command, "--dangerously-skip-permissions") + error_message = "claude_code_skip_permissions=true must append --dangerously-skip-permissions" + } +} diff --git a/image/Dockerfile b/image/Dockerfile index 0a29172..cc6c24d 100644 --- a/image/Dockerfile +++ b/image/Dockerfile @@ -165,9 +165,17 @@ RUN printf '#!/usr/bin/env bash\nddev php "$@"\n' > /usr/local/bin/php && \ RUN mkdir -p /home/linuxbrew/.linuxbrew && chown -R coder:coder /home/linuxbrew/ +# Homebrew's cache must live on the same filesystem as the Cellar +# (/home/linuxbrew/.linuxbrew) to avoid cross-device link (EXDEV) errors when +# brew hardlinks a downloaded bottle from cache into the Cellar. At runtime, +# /home/linuxbrew is mounted as a persistent per-workspace volume (see each +# template's template.tf), so this path stays on the same filesystem as the +# Cellar across workspace restarts too. +ENV HOMEBREW_CACHE=/home/linuxbrew/.cache/Homebrew + USER coder RUN /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -RUN /home/linuxbrew/.linuxbrew/bin/brew install \ +RUN mkdir -p "$HOMEBREW_CACHE" && /home/linuxbrew/.linuxbrew/bin/brew install \ bats-core \ bats-core/bats-core/bats-assert \ bats-core/bats-core/bats-file \ From 8a903127ce354ef5241a286334db3941cfafc1d3 Mon Sep 17 00:00:00 2001 From: Randy Fay Date: Sun, 26 Jul 2026 12:39:07 -0600 Subject: [PATCH 2/8] fix(drupal-contrib): stop tracking generated VERSION file drupal-core and freeform both gitignore their per-template VERSION file (it's copied from the root VERSION by the Makefile's push_template recipe before every `coder templates push`). drupal-contrib was missing that .gitignore entry, so its copy got committed by accident. Left tracked, every version bump would show up as a dirty uncommitted change in drupal-contrib specifically, and a stray `git checkout .`/`git clean` could silently revert it to a stale version. Add the matching .gitignore entry and untrack the file (kept on disk so the Makefile's cp step still works). Co-Authored-By: Claude Sonnet 5 --- drupal-contrib/.gitignore | 8 ++++++++ drupal-contrib/VERSION | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 drupal-contrib/.gitignore delete mode 100644 drupal-contrib/VERSION diff --git a/drupal-contrib/.gitignore b/drupal-contrib/.gitignore new file mode 100644 index 0000000..b1671c7 --- /dev/null +++ b/drupal-contrib/.gitignore @@ -0,0 +1,8 @@ +# Vault token file (not committed to Git) +vault-token.txt + +# VERSION is copied from root by Makefile before push +VERSION + +# Terraform working directory +.terraform/ diff --git a/drupal-contrib/VERSION b/drupal-contrib/VERSION deleted file mode 100644 index 1811f96..0000000 --- a/drupal-contrib/VERSION +++ /dev/null @@ -1 +0,0 @@ -v0.4 From 4926389b173ea7e0ce7c21cc66f97d46160bfea8 Mon Sep 17 00:00:00 2001 From: Randy Fay Date: Sun, 26 Jul 2026 21:02:20 -0600 Subject: [PATCH 3/8] fix(freeform): expose enable_claude_code as a per-workspace parameter enable_claude_code/claude_code_skip_permissions were plain Terraform variables, which are only settable by whoever pushes the template (via coder templates push --variable) and apply to every workspace on that template version -- they never show up in coder create/the web UI or coder update. Discovered on staging: the toggle was invisible everywhere a workspace owner would look for it. Convert both to coder_parameters (matching the existing project_names/ vscode_extensions pattern), so each workspace can opt in independently via `coder create --parameter enable_claude_code=true`, `coder update`, or the dashboard. Update tests to use override_data on the new data sources instead of setting Terraform variables directly, and correct the README accordingly. Co-Authored-By: Claude Sonnet 5 --- freeform/README.md | 11 ++++--- freeform/template.tf | 48 ++++++++++++++++++++---------- freeform/tests/validate.tftest.hcl | 34 ++++++++++++++------- 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/freeform/README.md b/freeform/README.md index 0fe1f13..0b4d97e 100644 --- a/freeform/README.md +++ b/freeform/README.md @@ -91,11 +91,12 @@ After `ddev restart`, the post-start hook updates `coder-routes.yaml` to include ### Claude Code (Remote Control) -Enable Claude Code remote control when creating or updating the workspace: +`Enable Claude Code` and `Claude Code: skip permissions` are workspace parameters (per-workspace, not template-wide) — toggle them in the Coder dashboard when creating or updating a workspace, or from the CLI: ```bash -coder create --template freeform myworkspace -# Select: enable_claude_code = true +coder create --template freeform myworkspace --parameter enable_claude_code=true +# or, on an existing workspace: +coder update myworkspace --parameter enable_claude_code=true ``` At startup, Claude Code is upgraded via Homebrew and a tmux session named after the workspace is started running `claude`. Attach to it either via the **Claude Code** app button in the Coder dashboard, or manually: @@ -104,7 +105,7 @@ At startup, Claude Code is upgraded via Homebrew and a tmux session named after tmux attach -t myworkspace ``` -The first attach triggers Claude Code's interactive OAuth login — no API key is configured by this template. To skip permission prompts entirely, also set `claude_code_skip_permissions = true` (off by default; use with caution). +The first attach triggers Claude Code's interactive OAuth login — no API key is configured by this template. To skip permission prompts entirely, also pass `--parameter claude_code_skip_permissions=true` (off by default; use with caution). > **Known limitation — shared session state.** `enable_claude_code` starts exactly one tmux session per workspace, named after the workspace, running a single `claude` process. Every device/client that attaches (via the "Claude Code" app button or `tmux attach -t `) is attaching to the *same* running session — that's what makes multi-device remote control possible in the first place. The tradeoff: there's no per-client context, so `cd`-ing or `git checkout`-ing from inside that shared session changes it for everyone attached. Do routine repo administration (branch switches, directory navigation) from a separate, ordinary terminal (the Coder web terminal or `coder ssh `) instead — leave the Claude Code tmux session alone for that. @@ -151,8 +152,6 @@ ddev logs -f # Follow logs | `cpu` | 4 | CPU cores (1–32) | | `memory` | 8 | Memory in GB (2–128) | | `enable_adminer` | false | Show Adminer app button (requires `ddev get ddev/ddev-adminer`) | -| `enable_claude_code` | false | Enable Claude Code remote control (tmux session + app button) | -| `claude_code_skip_permissions` | false | Append `--dangerously-skip-permissions` to the Claude Code session (requires `enable_claude_code`) | | `workspace_image_registry` | `index.docker.io/ddev/coder-ddev` | Base image registry | | `docker_gid` | 988 | Docker group GID on host | diff --git a/freeform/template.tf b/freeform/template.tf index 5d737fa..1f54dbc 100644 --- a/freeform/template.tf +++ b/freeform/template.tf @@ -95,6 +95,28 @@ data "coder_parameter" "vscode_extensions" { } } +data "coder_parameter" "enable_claude_code" { + name = "enable_claude_code" + display_name = "Enable Claude Code" + description = "Upgrade Claude Code via Homebrew at startup and run it in a tmux session named after the workspace, attachable from the dashboard. Interactive OAuth login is required on first attach (no API key is configured by this template)." + type = "bool" + form_type = "switch" + default = "false" + mutable = true + order = 2 +} + +data "coder_parameter" "claude_code_skip_permissions" { + name = "claude_code_skip_permissions" + display_name = "Claude Code: skip permissions" + description = "Append --dangerously-skip-permissions to the Claude Code session. No effect unless Enable Claude Code is also on." + type = "bool" + form_type = "switch" + default = "false" + mutable = true + order = 3 +} + locals { workspace_home = "/home/coder" selected_extensions = jsondecode(data.coder_parameter.vscode_extensions.value) @@ -111,6 +133,12 @@ locals { ? [for s in split(",", local._project_names_raw) : trimspace(s) if trimspace(s) != ""] : [data.coder_workspace.me.name] ) + + # coder_parameter values are always strings; compare rather than tobool() so an + # unrelated mock default (e.g. "[]" in Terraform tests) safely evaluates false + # instead of erroring. + enable_claude_code = data.coder_parameter.enable_claude_code.value == "true" + claude_code_skip_permissions = data.coder_parameter.claude_code_skip_permissions.value == "true" } variable "vscode_extensions" { @@ -183,18 +211,6 @@ variable "enable_adminer" { default = false } -variable "enable_claude_code" { - description = "Enable Claude Code remote control: upgrades Claude Code via Homebrew at workspace startup and starts one persistent tmux session (named after the workspace) running `claude`, attachable from any device. Interactive OAuth login is required on first attach (no API key is configured by this template)." - type = bool - default = false -} - -variable "claude_code_skip_permissions" { - description = "Append --dangerously-skip-permissions to the `claude` command started by enable_claude_code. No effect unless enable_claude_code is also true." - type = bool - default = false -} - resource "coder_agent" "main" { arch = "amd64" os = "linux" @@ -414,7 +430,7 @@ BASHCOMP echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc fi - %{if var.enable_claude_code~} + %{if local.enable_claude_code~} # --- Claude Code remote control (enable_claude_code) --------------------- echo "Setting up Claude Code remote-control session..." @@ -427,7 +443,7 @@ BASHCOMP fi CLAUDE_CMD="claude" - %{if var.claude_code_skip_permissions~} + %{if local.claude_code_skip_permissions~} CLAUDE_CMD="claude --dangerously-skip-permissions" %{endif~} @@ -582,12 +598,12 @@ resource "coder_app" "adminer" { # running `claude`. Shared by every client that attaches — see README for # the cd/git-checkout caveat. resource "coder_app" "claude_code" { - for_each = var.enable_claude_code ? toset(["claude-code"]) : toset([]) + for_each = local.enable_claude_code ? toset(["claude-code"]) : toset([]) agent_id = coder_agent.main.id slug = "claude-code" display_name = "Claude Code" icon = "/icon/terminal.svg" - command = "tmux attach -t $CODER_WORKSPACE_NAME || tmux new -s $CODER_WORKSPACE_NAME claude${var.claude_code_skip_permissions ? " --dangerously-skip-permissions" : ""}" + command = "tmux attach -t $CODER_WORKSPACE_NAME || tmux new -s $CODER_WORKSPACE_NAME claude${local.claude_code_skip_permissions ? " --dangerously-skip-permissions" : ""}" share = "owner" } diff --git a/freeform/tests/validate.tftest.hcl b/freeform/tests/validate.tftest.hcl index 08c4cd4..be82929 100644 --- a/freeform/tests/validate.tftest.hcl +++ b/freeform/tests/validate.tftest.hcl @@ -145,37 +145,51 @@ run "claude_code_off_by_default" { command = plan assert { condition = length(coder_app.claude_code) == 0 - error_message = "coder_app.claude_code must not be created when enable_claude_code=false" + error_message = "coder_app.claude_code must not be created when enable_claude_code is off" } } run "claude_code_enabled" { command = plan - variables { - enable_claude_code = true + override_data { + target = data.coder_parameter.enable_claude_code + values = { + value = "true" + } } assert { condition = length(coder_app.claude_code) == 1 - error_message = "coder_app.claude_code must be created when enable_claude_code=true" + error_message = "coder_app.claude_code must be created when enable_claude_code is on" } } run "claude_code_skip_permissions_off_by_default" { command = plan - variables { - enable_claude_code = true + override_data { + target = data.coder_parameter.enable_claude_code + values = { + value = "true" + } } assert { condition = !strcontains(coder_app.claude_code["claude-code"].command, "--dangerously-skip-permissions") - error_message = "claude_code_skip_permissions must default to false" + error_message = "claude_code_skip_permissions must default to off" } } run "claude_code_skip_permissions_enabled" { command = plan - variables { - enable_claude_code = true - claude_code_skip_permissions = true + override_data { + target = data.coder_parameter.enable_claude_code + values = { + value = "true" + } + } + override_data { + target = data.coder_parameter.claude_code_skip_permissions + values = { + value = "true" + } } assert { condition = strcontains(coder_app.claude_code["claude-code"].command, "--dangerously-skip-permissions") From e5e560620f432523c1e3db7746372d07f09a1793 Mon Sep 17 00:00:00 2001 From: Randy Fay Date: Sun, 26 Jul 2026 21:06:46 -0600 Subject: [PATCH 4/8] chore: bump VERSION to v0.5 for the Homebrew/Dockerfile changes Matches the image already rebuilt and pushed on amd64 with the HOMEBREW_CACHE relocation from this branch. Co-Authored-By: Claude Sonnet 5 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 1811f96..83b4ac5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.4 +v0.5 From 34be869a2ae98dd0ad25ff0237fa5bd8e731fa1e Mon Sep 17 00:00:00 2001 From: Randy Fay Date: Sun, 26 Jul 2026 21:10:11 -0600 Subject: [PATCH 5/8] fix: track per-template VERSION files in git instead of gitignoring them Twice in this branch, an untracked/inconsistently-tracked per-template VERSION file caused real confusion: drupal-contrib/VERSION was accidentally committed while drupal-core/freeform's were gitignored, and then a freeform template push picked up a stale root VERSION with no git-visible record of what was actually deployed. Track all three (they now match root at v0.5). `make push-template-*` still runs `cp VERSION