remote-workspace

Remote workspaces for coding agents, over plain SSH.

What it is

Set up your coding agent on one machine. It can then work on any machine you can SSH into.

The remote side runs one small binary that the agent drives over your existing SSH config. No daemon, no open port, no copying your project around. If ssh myhost works, this works.

coding agent │ ▼ remote-workspace-mcp (on your machine) │ │ JSON Lines over ssh stdin/stdout ▼ remote-workspace-server (on the remote, installed for you)

Install

You need two binaries on your own machine. The server is installed on the remote automatically when you add a workspace, so you never copy it yourself.

Download

Static builds for Linux x86_64, so the glibc version does not matter.

BASE=https://github.com/hesic73/remote-workspace/releases/latest/download

mkdir -p ~/.local/bin
for b in remote-workspace remote-workspace-mcp; do
  curl -fsSL "$BASE/$b-linux-x86_64-musl" -o ~/.local/bin/$b
  chmod +x ~/.local/bin/$b
done

remote-workspace --version

Checksums for every asset are published as SHA256SUMS on the same release.

Build from source

git clone https://github.com/hesic73/remote-workspace
cd remote-workspace
cargo build --release

install -m755 target/release/remote-workspace \
              target/release/remote-workspace-mcp ~/.local/bin/

Add a workspace

One command probes the host, installs the matching server, runs a real round trip against your directory, and only then records the workspace. The directory must already exist.

remote-workspace workspace add robot \
  --host robot@workstation \
  --root /home/robot/project
Adding workspace 'robot'
  SSH                    connected
  Remote platform        linux-x86_64
  Workspace root         valid
  Server                 installed 0.5.0
  Protocol               3
  Workspace probe        passed
  Fleet configuration    updated
Workspace 'robot' is ready.

If any step fails, nothing is recorded and the error names the layer that broke. Add as many workspaces as you like, on as many machines as you like.

Adding is for new workspaces. A name already in the fleet is refused; moving registered hosts onto a newer release is upgrading.

Connect it to your agent

One entry serves your whole fleet, so adding a workspace later needs no change here. Restart the host application afterwards, since MCP servers are loaded at startup.

Claude Code

claude mcp add remote-workspace -- remote-workspace-mcp

Claude Desktop

In claude_desktop_config.json. Use an absolute path, because a GUI app does not inherit your shell's PATH.

{
  "mcpServers": {
    "remote-workspace": {
      "command": "/home/you/.local/bin/remote-workspace-mcp"
    }
  }
}

Codex

In ~/.codex/config.toml:

[mcp_servers.remote-workspace]
command = "/home/you/.local/bin/remote-workspace-mcp"

Check it works

remote-workspace-mcp --check
fleet config /home/you/.remote-workspace/workspaces.toml ok: 2 workspace(s)
robot [robot@workstation:/home/robot/project]: ok
lab   [lab-gpu-1:/data/experiments]: ok

Your agent now has nine tools: list, read, create, edit and delete files, run commands, and transfer single files in either direction. One edit_file call carries up to a hundred replacements: they apply in order, and either all of them land or the file is left untouched. Every tool except list_workspaces takes a required workspace argument, so a call cannot land on the wrong machine by default. Inspecting what happened afterwards is a job for the remote-workspace CLI, not a tool the agent carries.

Making conda work

Every command runs in a fresh process, so conda activate in one call never leaks into the next. Say once what a command's environment should be, in a config file on the remote host:

# ~/.remote-workspace/config.toml on the remote machine
default_profile = "user"

[profiles.user]
shell = ["bash", "-lc"]
setup = """
. "$HOME/miniforge3/etc/profile.d/conda.sh"
conda activate myenv
"""

[profiles.raw]
shell = ["bash", "-c"]
setup = ""

Point the workspace at it when adding, with --config /home/robot/.remote-workspace/config.toml.

Prefer an explicit setup over an interactive shell. Asking for bash -lic does load your dotfiles, but an interactive shell with no terminal prints job control warnings on every command, and on zsh it can drag in prompt plugins that fail noisily. Sourcing what you need is quieter and more predictable. Shells also differ: bash reaches .bashrc through .profile even when not interactive, while zsh reads .zshrc only for interactive shells, so dropping -i on a zsh host without an explicit setup can silently change which python runs.

Upgrading

Two steps, in this order. Your own binaries first: the client only ever fetches the release matching its own version, so a stale client installs a stale server and cheerfully reports success.

BASE=https://github.com/hesic73/remote-workspace/releases/latest/download

for b in remote-workspace remote-workspace-mcp; do
  curl -fsSL "$BASE/$b-linux-x86_64-musl" -o ~/.local/bin/$b
  chmod +x ~/.local/bin/$b
done

remote-workspace workspace upgrade

The second command installs once per SSH identity, however many workspaces share it, and reports up to date without transferring anything when a host already matches. It never edits your fleet file. Restart the host application afterwards, since MCP servers are loaded at startup.

The upgrade never downgrades a server newer than itself, and an older one is upgraded rather than worked around. Nothing negotiates at connection time, so upgrade your own binaries and your workspaces together: a client and a server from releases that disagree about the protocol fail per request, not up front.

Where things live

Your fleet is ~/.remote-workspace/workspaces.toml on your own machine, written by workspace add and re-read whenever it changes, so a new workspace appears without restarting your agent.

On the remote, the operation log sits in ~/.remote-workspace/state/, outside your project. Nothing appears in git status, and a destructive command inside the workspace cannot take the history with it. The log records what happened; it does not reverse it, so keep anything that must survive a mistake in version control. Paths beginning @scratch/ address a transient area there for the agent's own working files; anything untouched for a week is deleted, so keep what matters in the workspace or pull it down with download_file.

To see which tools your agent actually reaches for, register the MCP with --log-dir ~/.remote-workspace/log and read it back:

remote-workspace stats

tool                calls   errors
run_command          1800       24
edit_file             678       37
read_file             201        5
list_directory          8        0

It is off by default, because a logged request carries the full text of every file your agent writes.

When something breaks

SymptomUsually
connect_failed SSH itself cannot reach the host. Try ssh <host> true. Anything needing a password prompt fails rather than hanging.
probe_failed, state directory is locked Usually not a fault: another session is using that workspace, and one server serves one workspace root at a time by design. A server left behind by a session that died with its network releases the lock about fifteen minutes after its last activity -- longer if a command it started is still running. server.jsonl in the state directory records when each server started and why it stopped, which dates the holder; the lock itself is released by the kernel, so a server that was killed leaves no exit line and no lock either.
Connected, but the wrong python A profile problem rather than a connection problem. Compare run_command under profile: "raw" with your default.
Edited workspaces.toml by hand, cannot connect Editing it yourself installs nothing and checks nothing. A wrong SSH username quietly produces paths that do not exist. Remove the entry and use workspace add.