Skip to content

feat: optional Claude Code self-hosted runner in the freeform template - #224

Merged
rfay merged 7 commits into
mainfrom
20261027_rfay_self_hosted_runner
Oct 27, 2026
Merged

rfay merged 7 commits into
mainfrom
20261027_rfay_self_hosted_runner

Conversation

@rfay

@rfay rfay commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds an optional Claude Code self-hosted runner to the freeform template, so sessions started on claude.ai/code run in a Coder workspace with its Docker and DDEV. Today this takes a hand-written ~/.coder-startup.sh. In one case its /tmp marker survived a workspace stop/start and silently kept the runner from starting.

Changes

  • New module modules/claude-self-hosted-runner, vendored into freeform only (make sync-claude-runner-module, part of sync-shared).
  • Two parameters: "Run a Claude Code self-hosted runner" (off by default) and "concurrent sessions" (1–4).
  • The user saves the environment secret once in ~/.claude-runner/environment-secret, so it never goes through Terraform. If the file is missing, the startup log and ~/WELCOME.txt say what to do.
  • The startup script starts one runner per concurrent session. Each runs in a restart loop in its own window of tmux session claude-runner, with --use-anthropic-git-proxy and --capacity 1. The runner refuses a larger capacity with Anthropic-managed git, so each runner gets its own base directory ~/workspace/runner-<n>, its own GIT_CONFIG_GLOBAL and its own log /tmp/claude-runner-<n>.log.
  • The runner deletes ~/.gitconfig and ~/.config/git before every session, so the session git identity and ignore list go in /etc/gitconfig, reinstalled on every start. For the same reason, the runner refuses to start when Claude Code remote control is also on.
  • It installs ~/.claude/skills/coder-shared-workspace, which the runner copies into every session. The skill covers:
    • stay out of other runners' checkouts
    • a per-session DDEV_XDG_CONFIG_HOME
    • no workspace-wide Docker/DDEV commands
    • unique project names
    • a flock around test suites that run ddev poweroff
    • disk cleanup limited to a session's own files
  • It installs claude-sessions, which lists live sessions with their checkout, branch and unpushed commits.
  • Docs in docs/user/using-workspaces.md, and an OpenSpec change in openspec/changes/add-claude-self-hosted-runner.

Related: ddev/ddev#9022 proposes letting remote sessions push their own claude/* branch.

Testing

  • terraform fmt -check -recursive, make validate and make test-templates pass. New freeform tests cover: off by default, capacity passed as --capacity, and refusal alongside remote control.
  • I rendered the module's startup snippet and the loop script and checked both with bash -n. I ran the enabled snippet with a fake home and a sudo shim, and it installed the skill, the loop script, claude-sessions and the git config.
  • On staging-coder.ddev.com, I pushed the template inactive and created a freeform workspace with the runner on and capacity 2. Then I checked three cases:
    • No secret: the runner doesn't start. The startup log and WELCOME.txt say how to add the secret.
    • Dummy secret, then a restart: the runner starts on its own in tmux session claude-runner, with capacity 2. The skill, claude-sessions and /etc/gitconfig are installed.
    • Registration fails with 401: the loop retries every 60s, not every 5s.
    • Real secret, concurrent sessions set to 2: two runners register, and two sessions run at the same time, one on ddev/ddev and one on ddev/coder-ddev. Each session has the skill, takes its git identity from /etc/gitconfig, and shows up in claude-sessions.
  • The staging tests found these problems, all fixed in this PR:
    • The "already running" check matched the startup script's own bash process.
    • A file created under umask 077 stayed group-readable because of the home directory's default ACL, so the docs now use chmod 600.
    • One runner with capacity 2 refuses to run with --use-anthropic-git-proxy, hence one runner per session.
    • --push-outcome-on-release would push with the workspace's own git credentials, which a runner workspace doesn't have, so I removed it.
  • Both sessions then resumed on the rebuilt workspace, which runs one runner per session. Each was picked up within about a minute of a new message and ran claude-sessions and the skill.

https://claude.ai/code/session_01UrTUMvjLtFGNseL9fh5s4o


Generated by Claude Code

…g the workspace

Add modules/claude-self-hosted-runner, vendored into all three templates.
A switch (default off) starts `claude self-hosted-runner` in a restart loop
in tmux session `claude-runner` on every workspace start, once the user has
saved ~/.claude-runner/environment-secret; the secret never goes through
Terraform. A second parameter sets the number of concurrent sessions (1-4).

The runner rewrites ~/.gitconfig and ~/.config/git before every session, so
the session git identity and ignore list go in /etc/gitconfig, reinstalled
on each start, and the runner refuses to start alongside remote control.

It also installs:
- ~/.claude/skills/coder-shared-workspace, copied into every session, with
  rules for sessions sharing one Docker daemon and DDEV install: worktrees
  instead of shared checkouts, no workspace-wide Docker/DDEV commands, and a
  flock around test suites that power off DDEV.
- claude-sessions, listing live sessions with checkout, branch and unpushed
  commits, and --push-outcome-on-release, so commits from repositories that
  forbid agents to push still reach the session's claude/ branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrTUMvjLtFGNseL9fh5s4o
The coder-shared-workspace skill now tells sessions to set
DDEV_XDG_CONFIG_HOME to a per-session directory seeded from ~/.ddev, so
project lists and global settings stay separate. Container names and the
shared router stay global, so unique project names and the test lock remain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrTUMvjLtFGNseL9fh5s4o
The Drupal templates set up one project for a person to work on; a runner
workspace is a general-purpose host, which is what freeform is for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrTUMvjLtFGNseL9fh5s4o
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
PR Preview Action v1.8.2
Preview removed because the pull request was closed.
2026-10-27 17:03 UTC

The "already running" check used pgrep -f 'claude self-hosted-runner',
which matched the startup script's own bash process, so on staging the
runner never started. Anchor the pattern on the program name.

The docs now use chmod 600 for the secret: the home directory's default
ACL leaves a file created under umask 077 group-readable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrTUMvjLtFGNseL9fh5s4o
With a revoked or wrong secret the runner exits within a second, and the
loop retried every 5s indefinitely. Wait 60s after a run shorter than a
minute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrTUMvjLtFGNseL9fh5s4o
On staging, a runner with --use-anthropic-git-proxy and --capacity 2 exits
at once: "--use-anthropic-git-proxy requires --capacity 1 (the proxy URL is
per-session and linked worktrees share origin)". Start one runner per
concurrent session instead, each with --capacity 1, its own base directory
~/workspace/runner-<n>, and its own GIT_CONFIG_GLOBAL, so one runner's git
config rewrite cannot break another's clone. Tested by hand on staging with
two runners and two sessions running at once.

Drop --push-outcome-on-release: under Anthropic-managed git it pushes to the
plain GitHub URL with the workspace's own git credentials, which a runner
workspace does not have, so the push would fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrTUMvjLtFGNseL9fh5s4o
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrTUMvjLtFGNseL9fh5s4o
@rfay
rfay merged commit 59b96da into main Oct 27, 2026
18 checks passed
@rfay
rfay deleted the 20261027_rfay_self_hosted_runner branch October 27, 2026 17:00
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.

2 participants