Canopy workspaces don't have to live on your laptop. Register an SSH-reachable host once, register a project path on that host, then run canopy new --on tower from anywhere. The heavy work — scripts.setup, scripts.run, the agent panes — happens on the host. The laptop becomes a thin client. One unified TUI views every workspace across every host.
- Heavy builds / heavy dependencies. Your laptop fans don't have to be the bottleneck. A 32-core tower does the bundle/install/migrate dance and you keep typing.
- Long-running agents. A
canopy new --on tower --prompt "..." --no-attachkeeps claude grinding even when you close your laptop. - Shared review boxes. A team SSH box can host workspaces for everybody; each developer keeps their own laptop-side state.
- OS isolation. Macs and Linuxes for the same project, without you switching machines.
| Tool | Where | Why |
|---|---|---|
| Canopy (v0.17.0+) | Laptop AND each remote host | Both ends speak the same wire protocol (canopy ls --json schema v3). |
| SSH (OpenSSH 8.x+) | Laptop | ControlMaster reuse keeps refresh fast. |
| SSH key auth to each host | Laptop ↔ host | Passwords break the BatchMode no-prompt contract. The --interactive add flow can run ssh-copy-id for you. |
| mosh (1.4+) | Both ends | Used by canopy switch --on <host> for suspend-tolerant attach. Fallback to ssh -t if mosh is missing on either side. |
Install canopy on the remote with the same one-liner you used locally:
ssh tower
curl -fsSL https://raw.githubusercontent.com/avinashjoshi/canopy/main/install.sh | sh
exit# Quick form (positional args, no probe)
canopy host add tower cassy@tower.tail.ts.net
# Guided form — probes connectivity, offers ssh-copy-id if key auth fails,
# verifies the remote canopy version
canopy host add --interactiveVerify:
canopy host lsNAME SSH TARGET PROJECTS
pi jarvis@a10i-pi-gateway.geep-carat.ts.net — (run `canopy project add ... --on pi`)
tower cassy@a10i-tower.geep-carat.ts.net 1 (canopy)
The host registry lives at ~/.canopy/hosts.json. Hand-edits round-trip cleanly.
canopy project add cravd /home/cassy/Work/cravd --on towercanopy project ls shows local + remote in one view:
HOST PROJECT PATH
local brain /home/avi/Work/brain
local canopy /home/avi/Work/canopy
local cravd /home/avi/Work/cravd
tower canopy /home/cassy/Work/canopy
tower cravd /home/cassy/Work/cravd
Naming convention: the project name on the remote should match the local project's directory basename so cwd-driven dispatch works without typing paths. (When you run canopy new --on tower from inside ~/Work/cravd, canopy resolves cravd against the host's registry.)
# From anywhere — uses the project path from the registry
canopy new --on tower
# With an opening prompt for the agent (fire-and-forget)
canopy new --on tower --prompt "fix the timezone bug" --no-attach
# One-off path override when you don't want to register a project
canopy new --on tower --remote-cwd /tmp/scratch-repoThe prompt travels safely: base64 encoded into a heredoc → umask-077 temp file on the remote → cleaned up on script exit. It never appears in ps aux on either machine.
canopy switch --on tower fix-the-bugUnder the hood that's mosh --ssh="ssh ..." -- tmux attach -t canopy/<branch>. Mosh's UDP transport tolerates laptop suspend, flaky wifi, and roaming between networks. If mosh isn't installed, canopy falls back to ssh -t tmux attach.
Once attached, the tmux statusline prefixes the workspace segment with a yellow @<host> pill (using the registered host nickname from hosts.json, not the remote's hostname). Two long-running sessions side by side — one local, one on tower — now look distinct at a glance, no more "wait, which box am I typing into?". The pill clears automatically when you re-attach to that same session locally (canopy unsets the tmux session env on local attach, so the pill never lies).
For multi-attach (you want to watch the session from two machines):
canopy switch --on tower fix-the-bug --shareThat sets CANOPY_NO_DETACH=1 over SSH, so the remote canopy switch doesn't kick the existing client.
Everything you can do locally works with --on:
canopy ls --on tower # workspaces on the host
canopy rm --on tower fix-the-bug # remote teardown
canopy retry --on tower broken-one # re-run scripts.setup remotelycanopy (no args) → press Tab to land on Remote hosts.
The tab lists every registered host with:
- Current canopy version (probed every 2 seconds via
canopy ls --json) with a yellow⇑badge when the host is behind your laptop (or⇓when it's ahead) — silent when matched - Last-seen timestamp
- Workspace count + per-host CPU / memory hints
- Agent badges for each remote workspace (the laptop now shows
💤/✋/⚡for remote rows, not just local) - Last error if a fetch failed (auth, network, version mismatch)
On first load — before any host has a cached snapshot — never-refreshed hosts animate a Braille spinner (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏, ~120 ms cadence) until their fan-out result lands, instead of showing a static · (never refreshed) for the full 3-second SSH window. Hosts with a cached snapshot keep their previous status. The tick loop stops itself once the fan-out settles (v0.21.1.0).
The same spinner glyph now also decorates each remote host's section header on the Workspaces tab during a refresh — the Hosts tab spinner only covered hosts without cached rows yet, so re-refreshing a host that already had rows looked silent. Spinners on both tabs animate from the same frame counter and clear together when results land (v0.21.5.0).
Verbs while on the tab:
| Key | Action |
|---|---|
enter |
Open host detail drawer (ssh-target, projects, last-error) |
n |
Open the in-TUI add-workspace picker (Fresh / Prompt / PR / Issue / Branch — full parity with local; loaders SSH gh pr list, gh issue list, and git for-each-ref against the remote project cwd) |
s |
Open an interactive ssh shell on the host (y/N confirm first; canopy refreshes when you exit) |
U |
Run canopy upgrade --yes on the host; output streams into the TUI |
S |
Run canopy use release on the host (recovery if it's running a DEV binary that refuses upgrade) |
a |
Re-offer ssh-copy-id (useful if the host lost key auth) |
d |
Remove the host from the registry |
f |
Narrow Global tab to this host's rows |
The Global tab mixes local and remote workspaces. Remote rows show a ↗ glyph in the TMUX column. Workspace verbs (d, K, B, R, i, enter) all work; some delegate over SSH transparently:
enter→canopy switch --on <host> <name>(mosh attach)d→canopy rm --on <host> <name>(withFfor force, no local safety preflight)K→ssh <host> tmux kill-session -t canopy/<branch>B→ auto-creates anssh -L <port>:localhost:<port>forward and runsxdg-openlocally — your laptop browser opens the remote dev serverR→canopy retry --on <host> <name>i→ diagnostic drawer fetched over SSH
If you press enter on a session another client is already attached to, the TUI now pops a y/N confirm before sharing/stealing. Skips the prompt when re-attaching to your own launching session.
Two flows, both on the Remote hosts tab:
U—canopy upgrade --yeson the host. Confirms first (no surprise SSH), then streamsgit pull && make installoutput into the TUI. Errors stay on-screen instead of disappearing inside an alt-screen suspend. When the failure is a permission problem on~/.canopy/srcor~/.local/bin(typical when a previous install ran assudo), the error names the offending directory and tells you tosudo chown -R $(whoami) …it — the misleading "local commits in source clone" hint is gone (v0.21.1.0). The remote shell snippet prepends~/.local/bin,/usr/local/go/bin,~/go/binto PATH and activatesmise/asdfdirectly, somake installfindsgoeven when the host's mise/asdf is wired through~/.bashrc(which a non-interactivebash -lskips) — fixes themake: go: No such file or directoryfailure on Arch/Omarchy/Ubuntu defaults (v0.21.7.0).S—canopy use releaseon the host. The recovery path when a host is running a dev binary andcanopy upgraderefuses with "Switch to the released canopy first."
U is hidden on hosts already running a DEV binary (it would error); S is hidden on hosts reporting a real semver (it would no-op). Both keys show up on hosts reporting (unknown) — that's the legacy state for hosts older than v0.17.1 that didn't emit version over the wire.
Hosts that are behind your laptop's canopy show a yellow ⇑ next to their version in the row; the host detail drawer (press enter) spells the same out as ⇑ upgrade available (your local: vX.Y.Z) — press U. Hosts ahead of your laptop render ⇓. Comparison suppresses silently when either side reports dev or (unknown).
- Wire protocol.
canopy ls --jsonschema v3 carriesmem_rss,cpu,hints,last_error_hint,agent_state, andcanopy_versionper workspace row. Backwards-compatible: older laptop parsers ignore unknown fields. - Refresher. The TUI's 2s tick fans out per-host goroutines with 3s deadlines, so one slow host can't block the others. SSH ControlMaster reuse keeps subsequent fetches near-zero latency.
BatchMode=yesprevents password prompts from hanging the Bubbletea render. - Agent classification.
agent.ClassifyOneShotruns on the remote side ofcanopy ls --json, so the 💤 / ✋ / ⚡ badges are accurate for remote rows without the laptop having to read the remote tmux pane. - Main rows. Remote
(main)rows show realrunning/not startedstatus and the actual default branch, becauseIsMain=trueandfillMainBranchesnow both happen on the remote side ofcanopy ls --json.
- Agent must be installed on the remote. Canopy can't ship
claudefor you. The remote needsclaude(or whateverscripts.agentpoints at) on PATH. - mosh ports. Mosh uses UDP ports 60000–61000 by default. If you're behind a firewall, either open the range or set
MOSH_TITLE_NOPREFIX=1and use the ssh fallback. - Single-laptop assumption. The host registry is laptop-local. If you want the same registry on a second machine, copy
~/.canopy/hosts.jsonover.
canopy host addprobe fails with auth error. Run the probe with--interactive— it offersssh-copy-idautomatically. Orssh-copy-id <ssh-target>by hand and re-add.- TUI shows
(unknown)version for a remote. That host is on canopy < v0.17.1.0 (older versions didn't emitcanopy_versionover the wire).UandSboth show on those rows so you can recover. - Refresh hangs the TUI on launch. Pre-v0.17.0.1 bug; upgrade your laptop canopy. The refresher now does all I/O inside the returned tea.Cmd closure.
- Remote row shows the wrong agent badge. Single-shot classification looks at the remote tmux pane on every refresh. If
claudecrashed to a shell, the badge correctly becomes·. Pressenterto re-attach and start it. canopy new --on towerfrom$HOMEfails with "needs a project". The TUI passes--remote-cwdresolved from the host registry; the CLI doesn't. Eithercdinto a project dir first or pass--remote-cwd /path/on/remoteexplicitly.
See troubleshooting.md for the wider catalogue.
getting-started.md— local-first install and first workspacecanopy-json.md— schema (same on local and remote)landscape.md— where remote dispatch sits next to mosh, sshfs, and friends