# Claude Focus Remote Workspaces
The `--remote` flag for `cf` (claude-focus) provisions a cloud workspace,
SSHs into it, and launches Claude Code there — same `cf` UX, remote compute.
> [!info] Same Mental Model, Remote Execution
> `cf --remote task-name` still creates one tmux session per task, with the
> same `CF_*` metadata, deterministic session ID, and `cfd`/`cfr`/`cfl`
> lifecycle. The difference is that the working directory is on a cloud VM
> instead of a local worktree.
## Providers
Two providers are implemented. Detection runs in a fixed order; the first
matching provider wins.
### Provider 1: Datadog Workspaces
Activated when the `workspaces` CLI is installed and the repo belongs to a DD
org.
**Detection (allowlist):**
- `command -v workspaces` succeeds
- Repo's GitHub org is in `CF_WORKSPACES_ORGS` (default: `DataDog ddoghq open-telemetry`)
| Aspect | Detail |
|--------|--------|
| **Create** | `workspaces create <name> -R <repo> -d kakkoyun/dotfiles -s zsh -y [-b <branch>]` |
| **SSH host** | `workspace-<name>` (via `~/.ssh/workspaces/<name>.config`) |
| **Repo location** | `~/src/<repo-name>/` |
| **Dotfiles** | Provisioned at creation time via `-d kakkoyun/dotfiles` — no extra setup needed |
| **SSH agent** | Forwarded by default — GitHub access works immediately |
| **SSH config** | Generated by `workspaces ssh-config <name>` after creation |
| **Delete** | `workspaces delete <name> --skip-known-hosts` |
### Provider 2: exe.dev
Activated for any repo NOT claimed by Datadog Workspaces — a denylist pattern.
All operations go through the SSH gateway at `exe.dev`; no separate CLI binary
is required.
**Detection (denylist):**
- Repo's GitHub org is NOT in `CF_WORKSPACES_ORGS`
- `ssh -o ConnectTimeout=5 -o BatchMode=yes exe.dev whoami` succeeds
| Aspect | Detail |
|--------|--------|
| **Create** | `ssh exe.dev new --name=<name> --json` — parses `ssh_dest` from JSON response |
| **SSH host** | Value of `ssh_dest` field in the JSON response (e.g., `myvm.exe.xyz`) |
| **VM naming** | `<repo>-r$(printf '%x' $(date +%s))` — hex Unix timestamp with `r` prefix |
| **Dotfiles** | Provisioned by `cf-provision-dotfiles` after bootstrap |
| **gh auth** | Forwarded via `gh auth token \| ssh <host> 'gh auth login --with-token'` |
| **Repo location** | `~/src/<repo-name>/` (cloned by `_cf_remote_setup_exedev`) |
| **Clone URL** | SSH URLs converted to HTTPS for reliability in non-interactive sessions |
| **Delete** | `ssh exe.dev rm <name>` |
**Why hex VM names:** exe.dev rejects names with purely numeric trailing
components. The `r` prefix and hex digits ensure at least one letter is always
present (e.g., `myrepo-r5f3a2b1c` rather than `myrepo-1749123456`).
## Lifecycle
### Datadog Workspaces
```
cf --remote task-name
→ detect provider: workspaces (org in CF_WORKSPACES_ORGS, CLI present)
→ workspaces create task-name -R <repo> -d kakkoyun/dotfiles -s zsh -y [-b <branch>]
→ workspaces ssh-config task-name (writes ~/.ssh config entry)
→ poll SSH until reachable (up to 60 s): ssh -o ConnectTimeout=2 -o BatchMode=yes workspace-task-name true
→ ssh workspace-task-name 'bash -s' < script/cf-bootstrap-remote
→ tmux new-session -s task-name -c <git_root>
→ CF_REMOTE_* env vars stored in tmux
→ ssh -A -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -t workspace-task-name 'cd ~/src/<repo> && claude'
cfr task-name
→ if pane command ≠ ssh*: reconnect with exponential backoff → claude --continue
cfd task-name
→ tmux kill-session
→ workspaces delete task-name --skip-known-hosts
```
### exe.dev
```
cf --remote task-name
→ detect provider: exedev (org not in CF_WORKSPACES_ORGS, exe.dev reachable)
→ ssh exe.dev new --name=task-name --json → parse ssh_dest
→ poll SSH until reachable (up to 60 s)
→ ssh <ssh_dest> 'bash -s' < script/cf-bootstrap-remote (installs node, tmux, claude)
→ ssh -A <ssh_dest> 'bash -s' < script/cf-provision-dotfiles (dotfiles, nvim, gh)
→ gh auth token | ssh <ssh_dest> 'gh auth login --with-token' (forward gh token)
→ ssh <ssh_dest> 'gh auth setup-git' (configure credential helper)
→ ssh <ssh_dest> 'git clone https://github.com/<org>/<repo>.git ~/src/<repo>'
→ tmux new-session -s task-name -c <git_root>
→ CF_REMOTE_* env vars stored in tmux
→ ssh -A -t <ssh_dest> 'cd ~/src/<repo> && claude'
cfr task-name
→ if pane command ≠ ssh*: reconnect with exponential backoff → claude --continue
cfd task-name
→ tmux kill-session
→ ssh exe.dev rm task-name
```
## --yolo Flag
```bash
cf --remote --yolo task-name
```
Passes `--dangerously-skip-permissions` to Claude Code on the remote VM.
Requires `--remote` — error if used locally.
- Stored as `CF_YOLO_MODE=true` in the tmux environment
- `cfr` preserves yolo mode on SSH reconnect: reconnects with
`claude --dangerously-skip-permissions --continue`
- `cfd` behavior is unchanged (cleanup is the same)
> [!warning] Remote-only
> `--yolo` is intentionally restricted to remote sessions. Running
> `--dangerously-skip-permissions` locally bypasses all permission prompts,
> which is unsafe for untrusted sessions. Remote VMs are disposable — the blast
> radius is contained.
## Bootstrap Script (cf-bootstrap-remote)
`script/cf-bootstrap-remote` runs on the remote host after creation.
Idempotent — each step checks before installing.
**Installs (in order):**
| Step | Tool | Method | Check |
|------|------|--------|-------|
| 1 | Node.js | nodesource apt repo (LTS) or brew | `command -v node` |
| 2 | tmux | apt or brew | `command -v tmux` |
| 3 | Claude Code | `npm install -g @anthropic-ai/claude-code` | `command -v claude` |
| 4 | SSH agent fix | Stable `~/.ssh/ssh_auth_sock` symlink in `~/.bashrc` | `grep -q ssh_auth_sock ~/.bashrc` |
| 5 | tmux config | `set -g mouse on; set -g monitor-bell on` appended to `~/.tmux.conf` | `grep -q 'mouse on' ~/.tmux.conf` |
| 6 | Notification hook | `~/.claude/settings.json` with terminal bell Notification hook | file does not exist |
The notification hook (step 6) is only written if the file doesn't already
exist. If `cf-provision-dotfiles` runs after bootstrap, the dotfiles symlink
replaces the bootstrap-created file.
The script is piped via SSH:
```bash
ssh <host> 'bash -s' < script/cf-bootstrap-remote
```
If the script file is not found at its expected path (e.g., running from a stow
symlink without the dotfiles repo), an inline fallback runs:
```bash
ssh <host> 'command -v claude || npm install -g @anthropic-ai/claude-code'
```
## Provisioning Script (cf-provision-dotfiles)
`script/cf-provision-dotfiles` runs after bootstrap on **exe.dev VMs** (which
start empty). Not needed for Datadog Workspaces, which auto-deploys dotfiles
at creation time.
**Invocation:** `ssh -A <host> 'bash -s' < script/cf-provision-dotfiles`
(`-A` required for SSH agent forwarding during the GitHub SSH host check)
**Tool tiers:**
| Tier | Tools | Failure mode |
|------|-------|-------------|
| Must-have | `tmux` (from bootstrap), `nvim` (installed here), `claude` (from bootstrap) | Abort (`exit 1`) |
| Should-have | `stow`, `gh` CLI | Warn and continue |
| Nice-to-have | `delta`, `diffnav`, `starship` | Silent fallback |
**What it does:**
1. Adds `github.com` to `~/.ssh/known_hosts` (prevents host key prompt during clone)
2. Installs must-have and should-have tools
3. Aborts if `tmux`, `nvim`, or `claude` are still missing after install attempts
4. Clones `https://github.com/kakkoyun/dotfiles.git` → `~/.dotfiles`
(or pulls latest if already present)
5. Removes `~/.claude/settings.json` if it is a regular file (created by bootstrap)
so stow can replace it with a symlink to the dotfiles version
6. Runs `make install-shared` to deploy shared dotfiles
7. Creates `~/.gitconfig.local` with `pager = less` fallbacks if `delta` is absent
8. Clears `~/.gitconfig.local` if `delta` is present (lets the dotfiles config win)
## Internals: Provider Plugin Architecture
Providers are zsh plugin files sourced by `claude.zsh` at shell load:
```zsh
# claude.zsh
source "${dir}/claude-dd-workspaces.zsh" 2>/dev/null || true
source "${dir}/claude-exedev.zsh" 2>/dev/null || true
```
Each provider implements this interface:
```
_cf_remote_provider_<name>_detect()
Print provider name + return 0 if available, return 1 otherwise.
_cf_remote_create_<name>(ws_name, repo_name, [branch])
Create the workspace/VM. Print SSH hostname to stdout. Return non-zero on error.
_cf_remote_setup_<name>(ssh_host, repo_name, [branch], [git_root]) [optional]
Post-bootstrap setup. Called after cf-bootstrap-remote completes.
```
**Adding a new provider:** create `claude-<name>.zsh` in the same directory as
`claude.zsh` with the detect and create functions. It will be sourced
automatically on next shell start.
## SSH Details
### Host Pattern
| Provider | SSH Host Format |
|----------|----------------|
| DD Workspaces | `workspace-<name>` |
| exe.dev | `ssh_dest` from JSON response (e.g., `myvm.exe.xyz`) |
### SSH Options Used
```
# Polling (availability check during bootstrap wait):
ConnectTimeout=2
BatchMode=yes
StrictHostKeyChecking=accept-new
# Session connection:
ServerAliveInterval=30
ServerAliveCountMax=3
-A (agent forwarding)
```
`BatchMode=yes` prevents hanging on interactive prompts during the SSH poll.
`StrictHostKeyChecking=accept-new` auto-accepts new host keys (safe for freshly
created VMs). `ServerAliveInterval=30` keeps the connection alive through
idle periods.
### Agent Forwarding
DD Workspaces enables SSH agent forwarding by default. exe.dev sessions use
`-A` explicitly. Agent forwarding allows `git push` to GitHub from inside the
remote session, as long as the local SSH agent has the key loaded.
For exe.dev, `gh auth` forwarding supplements SSH agent forwarding for HTTPS
git operations (which are more reliable in non-interactive sessions).
## tmux Metadata for Remote Sessions
Remote sessions store the standard `CF_*` set plus these remote-specific
variables:
| Variable | Example Value | Purpose |
|----------|--------------|---------|
| `CF_WORKTREE_PATH` | `remote:workspace-task-name` | `remote:` prefix identifies session type to `cfl`, `cfr`, `cfd` |
| `CF_BRANCH_NAME` | `main` | Branch the workspace was created on |
| `CF_GIT_ROOT` | `~/Workspace/Sandbox/myrepo` | Local repo root (for session context) |
| `CF_SESSION_ID` | `a1b2c3d4-...` | uuid5 of `repo/name` |
| `CF_BASE_BRANCH` | `upstream/main` | Resolved remote-tracking base |
| `CF_REMOTE_PROVIDER` | `workspaces`, `exedev` | Provider identifier |
| `CF_REMOTE_NAME` | `task-name` | Name passed to provider CLI for create and delete |
| `CF_REMOTE_HOST` | `workspace-task-name`, `myvm.exe.xyz` | SSH hostname |
| `CF_YOLO_MODE` | `true` | Whether `--dangerously-skip-permissions` is active |
The `remote:` prefix in `CF_WORKTREE_PATH` is how `cfl` and `cfr` distinguish
remote sessions from local worktree and workspace-mode sessions without an
extra variable.
## Cleanup
### Single session (cfd)
```bash
cfd task-name # interactive confirmation
cfd --force task-name # skip prompt
```
Prompts:
```
This will:
- Kill tmux session: task-name
- Delete remote workspace: task-name (workspace-task-name)
Continue? [y/N]
```
After killing the tmux session:
- **Workspaces:** `workspaces delete task-name --skip-known-hosts`
- **exe.dev:** `ssh exe.dev rm task-name`
`--skip-known-hosts` prevents `workspaces delete` from interactively modifying
`~/.ssh/known_hosts`.
> [!warning] No cfgc Support for Remote Sessions
> `cfgc` (garbage-collect) only scans `~/Workspace/.worktrees/` for local
> worktrees. Remote sessions are not included in GC. Use `cfd` to clean up
> remote sessions manually, or rely on `cfl`'s orphan detection to surface
> VMs with no matching tmux session.
## Troubleshooting
### OIDC Authentication (Workspaces)
DD Workspaces requires OIDC auth. If `workspaces create` fails with an auth
error:
```bash
workspaces auth login
```
### SSH Timeout
If `cf --remote` times out waiting for SSH (60 s):
```bash
workspaces list # check workspace status (DD Workspaces)
ssh exe.dev ls --json # check VM status (exe.dev)
ssh workspace-<name> true # manual reachability test
cat ~/.ssh/config # verify generated SSH config entry
```
### Bootstrap Failures
If Claude Code isn't installed after bootstrap:
```bash
ssh <host> 'which node; which claude; npm install -g @anthropic-ai/claude-code'
```
If provisioning failed on exe.dev:
```bash
ssh -A <host> 'bash -s' < script/cf-provision-dotfiles
```
### SSH Reconnect
If the SSH connection drops inside an existing remote session, `cfr` detects
the dead pane and reconnects automatically with exponential backoff (1 s → 2 s
→ 4 s → ... → max 60 s). To reconnect manually:
```bash
cfr task-name
# or from inside the tmux session:
ssh -A -t <host> 'cd ~/src/<repo> && claude --continue'
```
### gh auth Forwarding Failed (exe.dev)
If the provisioning step logs `gh auth forwarding failed`:
```bash
ssh <host> 'gh auth login' # authenticate interactively on the VM
ssh <host> 'gh auth setup-git'
```
### exe.dev Provider Not Detected
If `cf --remote` says no provider found:
```bash
ssh exe.dev whoami # test basic connectivity
ssh-add -l # verify SSH key is loaded in the agent
```