feat(freeform): add opt-in Claude Code remote-control support - #189
Merged
Conversation
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
|
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
enable_claude_code/claude_code_skip_permissionscoder_parameters to thefreeformtemplate, launchingclaude --remote-controlin a named tmux session connectable from claude.ai/code or the Claude mobile app, plus aclaude-herehelper (tmux respawn-pane -k) to retarget the shared session to a different project directory without re-authenticating.tmux -t "$CODER_WORKSPACE_NAME"resolves to the session's currently-active window rather than window 0, soclaude-here/thecoder_appattach command could silently act on the wrong pane.~/.bashrc-through-symlink bug (found in a real workspace) across all three templates: if~/.bashrcis 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 -checkcleanterraform validatepasses forfreeform,drupal-core,drupal-contribterraform testpasses for all three templates (freeform 15/15, drupal-core 7/7, drupal-contrib 7/7)enable_claude_code=true, logged in via OAuth, confirmed/rc activein the session, confirmedclaude-hereretargets the live authenticated session to a different directory with no re-auth (only the normal per-directory trust prompt)🤖 Generated with Claude Code