Skip to content

feat(freeform): add opt-in Claude Code remote-control support - #189

Merged
rfay merged 12 commits into
mainfrom
20260727_claude_remote_control_impl
Jul 28, 2026
Merged

feat(freeform): add opt-in Claude Code remote-control support#189
rfay merged 12 commits into
mainfrom
20260727_claude_remote_control_impl

Conversation

@rfay

@rfay rfay commented Jul 27, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds opt-in enable_claude_code / claude_code_skip_permissions coder_parameters to the freeform template, launching claude --remote-control in a named tmux session connectable from claude.ai/code or the Claude mobile app, plus a claude-here helper (tmux respawn-pane -k) to retarget the shared session to a different project directory without re-authenticating.
  • Fixes a tmux window-targeting bug found during staging verification: an untargeted tmux -t "$CODER_WORKSPACE_NAME" resolves to the session's currently-active window rather than window 0, so claude-here/the coder_app attach command could silently act on the wrong pane.
  • Fixes a ~/.bashrc-through-symlink bug (found in a real workspace) across all three templates: if ~/.bashrc is a symlink resolving outside $HOME, the startup script's env-var appends were writing straight through it into whatever it pointed at (e.g. a dotfiles repo) instead of a local file.

Test plan

  • terraform fmt -recursive -check clean
  • terraform validate passes for freeform, drupal-core, drupal-contrib
  • terraform test passes for all three templates (freeform 15/15, drupal-core 7/7, drupal-contrib 7/7)
  • Manually verified on staging-coder.ddev.com: pushed an inactive template version, created a workspace with enable_claude_code=true, logged in via OAuth, confirmed /rc active in the session, confirmed claude-here retargets the live authenticated session to a different directory with no re-auth (only the normal per-directory trust prompt)
  • Confirm the retargeted session is visible/connectable from claude.ai/code or the mobile app (client-side check, not automatable from here)

🤖 Generated with Claude Code

rfay and others added 3 commits July 27, 2026 14:12
Adds enable_claude_code and claude_code_skip_permissions coder_parameters
that launch `claude --remote-control` in a named tmux session on startup,
connectable from claude.ai/code or the Claude mobile app. Includes a
claude-here helper to retarget the session to the current directory, and
a terminal-tab coder_app fallback for first login/troubleshooting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
An untargeted "tmux -t $CODER_WORKSPACE_NAME" resolves to the session's
currently active window, not window 0 specifically. Verified on staging:
with a second tmux window present, claude-here silently respawned the
wrong pane instead of the actual Claude Code session. Pin all three tmux
references (claude-here's respawn-pane and the coder_app attach command)
to window 0 so they always target the session's original window.

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

If ~/.bashrc is a symlink resolving outside $HOME (e.g. a dotfiles repo
checked out under the home directory), every subsequent echo/sed/cat
append in these startup scripts writes straight through it into
whatever it points at. On at least one real workspace this silently
edited tracked files inside a developer's dotfiles repo instead of the
workspace's own local shell config.

Before the skeleton-copy/personalization steps run, detect that case
and replace the symlink with a real ~/.bashrc that sources the original
target, so later appends land in a local file instead of someone's
version-controlled dotfiles.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RrCPvPyTZYoEXzCoyvGXGq
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-28 00:15 UTC

rfay and others added 9 commits July 27, 2026 16:12
…eter description

Rewrite enable_claude_code's description to lead with what the user needs
to do (first-use login/trust steps, using claude-here to work in a project
directory) rather than the underlying tmux/CLI mechanism, since this text
is what actually shows up in the workspace creation form and Settings ->
Parameters page. Also notes the ~10-minute offline reconnect limit that
was called out as a documented tradeoff but never made it into the text.

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

Move the enable_claude_code / claude_code_skip_permissions parameters, the
computed remote-control command, the claude-here startup-script snippet,
and the fallback coder_app into modules/claude-remote-control/, then wire
it into drupal-core and drupal-contrib in addition to freeform, instead of
copy-pasting the feature into each template.tf.

Each template just does:
  module "claude_remote_control" {
    source   = "../modules/claude-remote-control"
    agent_id = coder_agent.main.id
  }
and splices ${module.claude_remote_control.startup_script} into its own
startup script, at the point after its npm-global setup.

Adds the same 3 test runs (disabled-by-default, enabled, enabled with
skip-permissions) to drupal-core and drupal-contrib's test suites,
mirroring freeform's existing coverage. Terraform test assertions can't
reach into a child module's internal resources directly, so the module
also exposes an `app_count` output for the enabled/disabled assertions.

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

`coder templates push --directory <dir>` only bundles that single directory,
so the module's original `../modules/claude-remote-control` source (which
worked fine locally and in CI, since both operate against a full git
checkout) resolved to a path that simply didn't exist in what actually got
uploaded to the Coder server - discovered when `make push-template-freeform`
failed with "Unable to evaluate directory symlink: lstat ../modules: no such
file or directory".

Fix: vendor a copy of modules/claude-remote-control into each template
directory and point each template's module source at the local
./modules/claude-remote-control copy instead. modules/claude-remote-control
at the repo root stays the canonical copy to edit; `make sync-claude-module`
(now also a prerequisite of validate, test-templates, and each
push-template-* target) copies it into all three template directories so
they can't drift out of sync.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ere sessions per directory

Found on real staging workspaces (dry-drupal-core, dry-drupal-contrib):

- The startup script's `if ! tmux has-session` guard assumed a pre-existing
  session meant claude was already running in it. Not true: the "Claude
  Code" app button's `tmux new-session -A` fallback can race ahead of the
  startup script and create a bare-shell session first, which the guard
  then left alone forever - claude never launched. Separately, a
  `claude --remote-control` process that never gets logged into/connected
  can exit on its own, killing the whole tmux server, with no way for the
  app button to recover it (it only attached-or-created-a-bare-shell).

  Fix: extract the "is window 0 actually running claude, and if not,
  (re)launch it" check into a shared ~/.local/bin/claude-ensure script,
  called both by the startup script and by the coder_app's command before
  it attaches - so either path can revive a dead or bare session, not just
  create one once at boot.

- claude-here always relaunched under the exact same --remote-control name,
  so after retargeting to a new directory there was no way to tell the new
  (live) session apart from the old (now-dead - a respawn never resumes the
  prior one) sessions of the same name in the claude.ai/code list. Now
  names each relaunch `$CODER_WORKSPACE_NAME-<directory-basename>`; the
  tmux session name itself (used for all tmux targeting) stays fixed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…on't hang CI

Adding enable_claude_code/claude_code_skip_permissions coder_parameters
broke every CI integration-test `coder create` invocation that didn't
explicitly enumerate every parameter: any coder_parameter without an
explicit --parameter value gets interactively prompted for, and on
non-interactive CI stdin that prompt immediately hits EOF ("error:
prepare build: EOF"), failing the job.

--use-parameter-defaults tells the CLI to fall back to each unspecified
parameter's default instead of prompting, so this doesn't need to be
re-fixed the next time a template gains a new optional parameter.
Verified locally against staging-coder with the exact drupal-core CI
flag set (no --parameter for the two new params): create succeeds
without any prompt.

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

Reproduced twice on a real staging workspace: if nobody logs into the
initial claude --remote-control session before its idle timeout, the
process exits - and since it was the only tmux pane, the whole tmux
server exits with it, leaving no session at all for claude-ensure (or
the app button) to detect and revive.

Set remain-on-exit on the session so a dead claude leaves a dead pane
behind instead of taking the server down - claude-ensure now checks
pane_dead in addition to pane_current_command and respawns either way.

Also: the only sign Claude Code was enabled and needed action was a
terse one-line "session ready" status buried in the build log. Print a
clearer instruction block (open the app button/SSH in to finish login;
cd + claude-here for a project directory) and append it to WELCOME.txt
so it's visible again on every later login, not just a one-time
scrollback.

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

$HOME is already an accepted workspace-trust boundary (established once at
first login), but claude's trust store (~/.claude.json, projects.<path>
.hasTrustDialogAccepted) is keyed per exact path, not inherited by
subdirectories. So every claude-here retarget into a new project directory
under $HOME re-triggered the trust dialog - with nobody there to answer it,
since claude-here just launches a background tmux pane. Remote control
wouldn't come online until someone happened to go check and accept it
manually, which is easy to forget and easy to mistake for a dead session.

claude-here now pre-marks the target directory as trusted via jq before
launching, for any directory under $HOME - the same boundary already
implicitly trusted. Directories outside $HOME are left alone (still
prompt normally), with an explicit reminder printed in the invoking
shell (not the respawned pane, so it's actually seen) that a manual
trust step is needed there.

Verified against a real staging workspace: patching hasTrustDialogAccepted
before launch skips the dialog entirely - straight to /remote-control is
active with a fresh session URL.

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

The CLAUDEHERE/CLAUDEENSURE heredoc terminators had leading whitespace left
over from the outer Terraform <<-EOT heredoc's source indentation. Terraform
doesn't strip it (a blank line early in the heredoc body makes the
common-indentation prefix empty, so no dedent happens at all), and bash's
own <<- only strips leading tabs from the terminator line, not spaces - so
the nested heredocs were never terminated. bash then silently swallowed the
rest of the startup script as heredoc body until reaching real EOF, at which
point the whole script failed with a syntax error and nothing after the
first `cat > claude-here <<-'CLAUDEHERE'` line ever ran.

This was missed because prior verification tested a manually-installed copy
of the script content, not the actual Terraform-rendered startup_script -
the first real workspace rebuild against the new template hit it on all
three templates (freeform, drupal-core, drupal-contrib) identically, since
they all share this module.

Fixed by placing both terminators at column 0.
…PATH lookup

`claude` is installed via Homebrew (linuxbrew) at
/home/linuxbrew/.linuxbrew/bin/claude, which is only added to PATH in
~/.bashrc - sourced for interactive shells only. tmux new-session and
respawn-pane run their command directly (not through a login/interactive
shell), so a bare `claude` invocation fails with "command not found" the
first time a session is created at boot. This was previously masked because
the heredoc bug (prior commit) always killed the startup script before
reaching this point in a fresh container, and every live test session had
so far been hand-created over an interactive SSH connection where PATH was
already correct.

Fixed by invoking the absolute path to the claude binary everywhere the
module launches it (remote_cmd and the claude-here tmux respawn-pane
command), instead of depending on PATH inheritance through tmux.
@rfay
rfay merged commit 7006955 into main Jul 28, 2026
16 checks passed
@rfay
rfay deleted the 20260727_claude_remote_control_impl branch July 28, 2026 00:14
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