# 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 ```