Remote workspaces for coding agents, over plain SSH.
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.
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.
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.
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/
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.
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 mcp add remote-workspace -- remote-workspace-mcp
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"
}
}
}
In ~/.codex/config.toml:
[mcp_servers.remote-workspace]
command = "/home/you/.local/bin/remote-workspace-mcp"
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.
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.
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.
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.
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.
| Symptom | Usually |
|---|---|
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. |