Skip to content

feat(freeform): opt-in Claude Code remote control; fix Homebrew persistence across restarts - #184

Closed
rfay wants to merge 8 commits into
mainfrom
20260726_claude
Closed

feat(freeform): opt-in Claude Code remote control; fix Homebrew persistence across restarts#184
rfay wants to merge 8 commits into
mainfrom
20260726_claude

Conversation

@rfay

@rfay rfay commented Jul 26, 2026

Copy link
Copy Markdown
Member

Summary

Bug fix 1 — Homebrew upgrades could fail outright (EXDEV cross-device link error).
/home/linuxbrew/.linuxbrew (the Homebrew Cellar) lives on the container's image filesystem, but Homebrew's default cache path ($HOME/.cache/Homebrew) resolves to /home/coder/.cache/Homebrew -- a separate, volume-mounted filesystem. Renaming/hardlinking a downloaded bottle across that boundary throws EXDEV. Fixed by relocating HOMEBREW_CACHE via a Dockerfile ENV so it always sits on the same filesystem as the Cellar.

Bug fix 2 — Homebrew upgrades didn't persist across workspace restarts.
docker_container.workspace is destroyed and recreated fresh from the image on every workspace stop/start, so anything installed/upgraded on the container's own root filesystem (including Homebrew) was silently lost on the next restart -- only the bind-mounted /home/coder and the dind-cache volume survived. Fixed in all three templates (freeform, drupal-core, drupal-contrib) by adding a new per-workspace docker_volume mounted at /home/linuxbrew, matching the existing dind-cache pattern. Docker auto-populates a fresh named volume from the image's existing directory contents on first mount, so no manual seed-copy step is needed -- just an ownership fix mirroring the existing /home/coder chown.

New feature — opt-in Claude Code remote control (freeform template only).
Two new variables, enable_claude_code and claude_code_skip_permissions (both default false). When enabled, the startup script upgrades Claude Code via Homebrew and starts a tmux session named after the workspace running claude -- a strict 1:1 workspace-to-session correspondence. A new coder_app using Coder's native command attribute (the same mechanism used for terminal-style apps elsewhere) lets you attach from the dashboard with one click -- no ttyd/wetty, no extra daemon or port, consistent with this repo's minimal/infra-only philosophy. --dangerously-skip-permissions is a separate, off-by-default toggle, never bundled into the base feature. Login is interactive OAuth only -- no API key variables are wired in.

Known, documented limitation: the tmux session is shared by every attached client, so cd/git checkout from inside it affects everyone attached -- routine repo administration should be done from a separate, ordinary terminal instead.

Test plan

  • terraform fmt -recursive
  • terraform validate for freeform, drupal-core, drupal-contrib
  • terraform test for all three templates (16/7/7 assertions passing, including new coverage for the linuxbrew volume and the Claude Code feature toggles)
  • Base image rebuilt and pushed on an amd64 host with the HOMEBREW_CACHE Dockerfile change
  • coder update an existing freeform workspace and confirm brew upgrade survives a stop/start cycle
  • Enable enable_claude_code on a freeform workspace, confirm the "Claude Code" app attaches to a tmux session named after the workspace, and confirm interactive OAuth login works

🤖 Generated with Claude Code

rfay and others added 2 commits July 26, 2026 12:30
…ew 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-27 12:01 UTC

rfay and others added 6 commits July 26, 2026 21:02
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 <noreply@anthropic.com>
Matches the image already rebuilt and pushed on amd64 with the
HOMEBREW_CACHE relocation from this branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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 <template>/VERSION` before pushing -- required because the
Coder provisioner only receives the contents of --directory <template>, not
the repo root -- but that copy now produces a normal, reviewable git diff
instead of being silently discarded or drifting unnoticed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The startup script and coder_app both launched plain `claude` (or with
--dangerously-skip-permissions) -- Remote Control was never enabled at all,
despite that being the entire point of the feature. Found on staging: the
session worked but wasn't connectable from claude.ai/code or the mobile app.

Add --remote-control $CODER_WORKSPACE_NAME to both the startup script's
CLAUDE_CMD and the coder_app's fallback attach/new command, so the tmux
session actually registers as a named Remote Control session. Correct the
README: the tmux/app-button terminal is a local fallback for first login and
troubleshooting, not the intended way to use the feature -- that's
claude.ai/code or the Claude mobile app, where the session shows up by name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ate copies

Each template read its own local VERSION file (file("${path.module}/VERSION"))
because coder templates push only ships the contents of --directory <template>
to the provisioner -- it never had access to the repo root. That meant three
physical copies kept in sync by a Makefile `cp` step, which already caused two
real bugs this branch: an untracked/stale copy after a manual push, and
drupal-contrib's copy being accidentally committed while the other two were
gitignored.

Drop the per-template copies entirely. Each template.tf now just uses
var.image_version directly; the Makefile passes it explicitly via
--variable image_version=$(VERSION) (already computed from the root VERSION
file) when pushing. Root VERSION is now the only copy anywhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nup sweep

cleanup-deleted-workspaces.sh's orphan detection only matched
coder-<owner>-<workspace>-dind-cache -- the new persistent linuxbrew volume
was invisible to it, so an orphaned one (from an interrupted destroy, state
desync, etc. -- exactly the failure modes this script is a safety net for)
would never be flagged or cleaned up. Match both known suffixes, and note
in-line that any future persistent per-workspace volume needs to be added
here too.

Also tightens a comment in workspace-lifecycle-cleanup.sh that attributed
volume cleanup to "the destroy provisioner" -- the null_resource destroy
provisioner in each template only removes the host-bind-mounted directory;
volumes are removed because Terraform destroys them along with the rest of
the workspace's resource graph, which is a separate mechanism.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@rfay

rfay commented Jul 27, 2026

Copy link
Copy Markdown
Member Author

This had too much stuff in it. Closing, breaking into multiple PRs.

@rfay rfay closed this Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant