Skip to content

Add remote SSH sessions: remote projects, indexing/search/resume, IDE over SSH - #78

Open
HAN-oQo wants to merge 3 commits into
doctly:mainfrom
HAN-oQo:feat/remote-ssh
Open

Add remote SSH sessions: remote projects, indexing/search/resume, IDE over SSH#78
HAN-oQo wants to merge 3 commits into
doctly:mainfrom
HAN-oQo:feat/remote-ssh

Conversation

@HAN-oQo

@HAN-oQo HAN-oQo commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds remote SSH session support to Switchboard. Today Switchboard is local-only — it reads ~/.claude/projects off the local disk and spawns terminals against local shells. This PR lets you run Claude Code (and shells) on remote hosts over SSH, with remote hosts + directories treated as first-class projects, their past sessions indexed & searchable, click-to-resume on the host, and (opt-in) the remote CLI's diffs/file-opens rendered in Switchboard's local side panel.

Delivered in three phases (each a self-contained commit); this single PR combines them.

Screenshots use redacted demo hosts (gpu-server, build-box, *.example.com); no real infrastructure is shown.

Phase 1 — Remote sessions & host management

Add Project gains a Local folder | Remote (SSH) choice. Pick a host, choose a remote directory, and it appears in the sidebar with an SSH badge; its + launches Claude/shell on that host. Connect verifies/authenticates in place (spinner → green; popup only when the host needs a password/passphrase or first-time host-key confirmation — never a raw terminal). A remote directory Browse mirrors the local folder picker. ~/.ssh/config hosts import automatically; you can also add hosts manually (identity file + extra -o options) and optionally write them back to ~/.ssh/config. Connections are multiplexed (ControlMaster) so you authenticate once per host.

Add Project — Remote (SSH)

Password prompt

Host key verify

Remote directory browser

Add SSH Host

Remote Hosts settings

Phase 2 — Indexing, search & resume

A connected host's past sessions are indexed (metadata over the existing SSH connection — no transcript copied to disk) so they persist in the sidebar, are searchable, and resume/fork in a terminal on the host. Auto-discovered by each session's cwd, grouped by remote directory, with the SSH badge + Claude logo + a ⟳ refresh. Full-text search covers remote transcripts; "View messages" streams a remote transcript live over SSH.

Remote sessions grouped in the sidebar Searching remote sessions

Phase 3 — IDE emulation over SSH

Opt-in: a remote Claude session pushes proposed diffs / file-opens into Switchboard's local side panel, exactly like a local session (review, accept, edit-then-accept, reject). It reverse-forwards the local IDE WebSocket port to the remote host (ssh -O forward -R), writes an IDE lock file there so the remote claude --ide connects back through the tunnel, and sources the diff's old side from the remote via ssh cat (the new side comes from the CLI). Off by default — the tunnel exposes the local IDE port on the remote host (protected by a per-session token), so it's enabled per your trust.

This phase also unifies the remote New Session popover and Configure dialog with the local ones (same labels/buttons; only justified differences remain — a Remote Directory picker, and Worktree/Chrome hidden as local-machine features; Pre-launch Command now applies to remote too).

IDE integration over SSH setting

Remote diff reviewed in the local side panel

Remote Configure dialog matches local

How it works

  • Transport: an SSH profile mirrors the existing WSL "wrapped command" pattern; the remote command runs in a login+interactive bash, single-quoted to survive ssh's argv re-join + remote-shell re-parse. One ControlMaster socket per host is reused by Browse, indexing, resume, and the IDE tunnel.
  • Model A: a project is local or remote. Remote projects persist in settings; sessions group under a synthetic ssh://<host>/<dir> path.
  • No local mirror: transcripts are read on demand over SSH (indexing metadata, View messages, and the diff's old side) — nothing is copied to local disk.
  • Auth: interactive PTYs let the user answer password / passphrase / passkey / 2FA / host-key prompts; Switchboard never stores secrets.

Data / settings

  • Additive migration adds a source column to session_cache + search_map (NULL = local; no cache wipe). All folder-scoped queries are source-isolated.
  • New setting remoteIde (default off).

Testing

  • Unit tests (npm test, run under Electron's node to match the app's better-sqlite3 ABI): SSH arg assembly + ~/.ssh/config parsing + quoting/transport (Phase 1); JSONL parsing, listing/framing/diff/cwd parsers, the source migration + isolation, sync orchestration, sidebar grouping (Phase 2); reverse-forward/cancel arg builders, the IDE lock script, the injected old-file reader (Phase 3). Full suite green.
  • The v3→v4 migration was verified against a copy of a real DB (rows preserved, source backfilled to local).
  • Manual: connect → launch/resume remote sessions; past sessions appear + are searchable + persist across restart; with the IDE setting on, a remote session's diff appears in the local panel with both sides populated, and accept-edited writes on the remote; no leftover remote lock on exit.

Roadmap

  • Phase 1 — Remote sessions & host management: launch Claude/shell on remote hosts; hosts + directories as first-class projects; interactive Connect; remote Browse; host config.
  • Phase 2 — Indexing, search & resume: past remote sessions indexed (metadata over SSH, no disk copy), searchable, persisted, and resumable/forkable on the host. Adds a source column.
  • Phase 3 — IDE emulation over SSH: reverse-forward the IDE socket so remote proposed diffs / file opens land in the side panel (behind a setting); unify remote/local session UI.

(Remote Control — claude --remote-control for live cross-device mirroring — is orthogonal to local-vs-remote and is tracked separately, not part of this PR.)

🤖 Generated with Claude Code

HAN-oQo and others added 3 commits July 13, 2026 18:33
Run Claude Code and shells on remote hosts over SSH. Remote hosts and directories
are first-class projects alongside local ones — no local-only assumption anymore.

- shell-profiles + remote-hosts: SSH shell profiles (mirrors the existing WSL
  wrapped-command pattern), ~/.ssh/config parsing, remote command assembly
  (single-quoted to survive ssh's argv re-parse), ControlMaster multiplexing.
- main.js: open-terminal remote branch (skips local-path checks, local cwd=$HOME,
  no MCP/shim), plus IPC for remote projects, interactive connect, remote dir
  browse, host save/test, and write-to-~/.ssh/config.
- session-cache: persisted remote projects injected into the sidebar; live remote
  sessions group under them (remote Claude reads as a Claude session).
- renderer: Add Project (Local | Remote SSH) with "+ Add new host", inline Connect
  with structured password / host-key popups (no raw terminal), remote directory
  browser, host management in Settings, SSH badges.
- 30 unit tests (node --test).

Auth stays with the user: passwords/passphrases/passkeys/host-key confirmations are
entered by the user; nothing is stored. Remote session indexing/search (Phase 2),
fork/resume on the remote (Phase 3), and IDE-over-SSH (Phase 4) are deferred — see
the PR description.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds on Phase 1 (doctly#76). Remote sessions previously appeared in the sidebar only
while live; this indexes a connected host's PAST sessions so they persist, are
searchable, and resume in a terminal — with no transcript copied to local disk.

- read-session-file: extract pure parseSessionContent(content, meta), shared by
  local and remote indexing (identical summary/title/search behavior).
- remote-index (new): metadata-over-SSH sync over the live ControlMaster socket.
  Lists a host's ~/.claude/projects/*.jsonl, auto-discovers a remote project per
  session from its cwd (ssh://<label>/<dir>), fetches only new/changed transcripts
  (length-framed stream), parses in memory, upserts cache + FTS tagged source=host,
  prunes vanished sessions. Pure helpers are unit-tested.
- db: add `source` column to session_cache + search_map (NULL = local; additive
  migration, no cache wipe). Source-scoped queries isolate local vs remote rows.
  DB path overridable via env for tests.
- main: sync-remote-host IPC + runRemoteSync; triggers on connect, manual refresh,
  remote-session exit, and a background sweep at startup (status surfaced to the
  status bar). read-session-jsonl streams remote transcripts live over SSH
  (View messages) with a needsConnect fallback. remove-project drops a remote
  project's indexed rows.
- Click-to-resume parity: a remote session resumes in a terminal via
  `cd <dir> && claude --resume "<id>"` on the host (fork supported); the resume
  dir is derived from the ssh:// projectPath. Remote Control reconnects
  automatically on resume when the session had it.
- sidebar/app: past remote sessions show the SSH badge + Claude logo; a refresh
  (⟳) control on remote project headers.
- test: run under Electron's node (matches the app's better-sqlite3 ABI).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds on Phase 2 (doctly#77). Opt-in: a remote Claude session can push proposed diffs
/ file-opens into Switchboard's local side panel, exactly like a local session.
Off by default — it reverse-forwards the local IDE port to the remote host
(token-protected).

IDE over SSH:
- remote-ide.js (new): pure command builders — reverse/cancel `ssh -O forward -R`
  over the shared control master, the remote IDE lock-file script (writes
  ~/.claude/ide/<port>.lock with $PWD + authToken after cd), remote lock cleanup,
  remote `cat`, and a candidate-port picker. Unit-tested.
- mcp-bridge.js: startMcpServer takes opts.readOldFile; openDiff/openFile read the
  "old" content through it (new readOldContent seam). Remote sessions inject an
  ssh-cat reader so the diff's old side reflects the remote file (new side comes
  from the CLI). Local unchanged.
- remote-hosts.js: buildRemoteCommand gains a preExec slot (cd && <preExec> &&
  exec) so the IDE lock is written with the project dir as $PWD.
- main.js: when the remoteIde setting is on for a remote claude session, bring up
  the control master, start the local MCP server, reverse-forward a free remote
  port (retry candidates), write the remote lock + export CLAUDE_CODE_SSE_PORT,
  add --ide. On exit: cancel the forward + rm the remote lock + shutdown the
  server. cleanStaleLockFiles runs at startup. If the tunnel can't be set up the
  session still runs (no --ide).
- Settings: "IDE integration over SSH" toggle (global, default off; per-project
  override honored).

Unify remote session UI with local (no reason to differ):
- The remote New Session popover now uses the same labels/icons/order as local
  (Claude / Claude (Configure...) / Terminal) — the SSH badge + host:path already
  mark it remote. Handlers branch on project.remote.
- The remote Configure dialog matches local: same "Cancel / Start" buttons (no
  more "Open shell"/"Start Claude"). Justified differences remain — a Remote
  Directory picker, and Worktree/Chrome hidden (local-machine features).
- Pre-launch Command now applies to remote too (prefixes the remote claude
  command), since it is meaningful over SSH unlike Worktree/Chrome.

Renderer diff panel is unchanged — remote diffs flow through the same IPC.
Tests run under Electron's node (matches the app's better-sqlite3 ABI).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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