ssh-client-relay lets a client delegate selected SSH connections to another
SSH-reachable machine. The relay machine runs the final SSH client and can reuse
credentials or a ControlMaster socket that exists only on that machine.
client application -> local ssh-client-relay -> SSH to relay
-> SSH client on relay -> final SSH server
All other destinations continue to use the client's normal OpenSSH executable. This makes the wrapper suitable as a global SSH runtime for applications such as VS Code Remote SSH while changing the route for only one configured target.
The client wrapper sends a protocol marker, argument count, and NUL-delimited
SSH arguments to a fixed helper on the relay. The helper reconstructs the exact
argument array without evaluating it as shell source, then executes the relay's
/usr/bin/ssh. After the argument frame, the same channel transports the inner
SSH process's stdin and stdout.
Dynamic forwarding needs one additional bridge. An inner -D PORT listener is
created on the relay, while applications such as VS Code expect it on the
client. The wrapper therefore adds an outer -L PORT:127.0.0.1:PORT forward so
the client-local port reaches the relay-local SOCKS listener.
This requires two independent connections:
- The client must be able to SSH to the relay.
- The relay must be able to SSH to the final server.
The second connection may reuse a relay-owned ControlMaster, but that is not a requirement.
Measured performance and methodology are documented in BENCHMARKS.md.
Before deployment, review the security model and known limitations. The relay is a fully trusted machine, not an end-to-end opaque transport.
- Bash and OpenSSH on the relay
- A working, preferably non-interactive SSH login from client to relay
- A working SSH configuration or route from relay to the final server
- On a Linux client: Bash, Perl, OpenSSH
sshandscp, andinstall - On a Windows client: Windows PowerShell 5.1 or later and Windows OpenSSH
The relay must run Linux because the helper uses Bash and the intended authentication reuse mechanism is an OpenSSH ControlMaster Unix socket. The client may run Linux or Windows; WSL is not required for the Windows client.
Supply the relay's SSH name, the client-facing target alias, and the target name understood by the relay:
./install.sh RELAY_HOST TARGET_ALIAS TARGET_HOSTFor example:
./install.sh ssh-relay compute compute.example.orgEnvironment variables are also supported:
RELAY_HOST=ssh-relay \
TARGET_ALIAS=compute \
TARGET_HOST=compute.example.org \
./install.shThe installer creates:
~/.local/bin/ssh-client-relayon the client~/.config/ssh-client-relay/configon the client~/.local/bin/ssh-client-relay-helperon the relay
RELAY_HOST must resolve through DNS or the client's SSH configuration.
If REMOTE_HELPER is overridden, it must be a simple path relative to the
relay user's home directory, such as .local/bin/ssh-client-relay-helper.
Absolute paths, traversal components, whitespace, and shell metacharacters are
rejected.
Run Windows PowerShell from the repository directory. If script execution is restricted, use a process-scoped bypass:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\install-windows.ps1 `
ssh-relay compute compute.example.orgThe Windows installer:
- Compiles
windows\SshClientRelay.csinto a native Windows-launchable .NET console executable using PowerShell's built-in C# compiler. - Writes
%USERPROFILE%\.config\ssh-client-relay\config.windows. - Copies the shared Bash helper to the Linux relay.
It installs the client executable at:
%USERPROFILE%\bin\ssh-client-relay.exe
No WSL, Visual Studio, separate .NET SDK, Python, or third-party package is
required. The Windows client uses ssh.exe and scp.exe from Windows OpenSSH.
During an upgrade, the installer validates a staged executable and waits up to 30 seconds for active relay processes to exit before replacing the installed file atomically. It never kills VS Code or removes the working executable on a failed upgrade. Close active remote windows before upgrading, or adjust the wait:
.\install-windows.ps1 ssh-relay compute compute.example.org `
-WaitForExitSeconds 120Using the example configuration:
~/.local/bin/ssh-client-relay -O check compute
~/.local/bin/ssh-client-relay compute hostname
printf 'stream-test\n' | ~/.local/bin/ssh-client-relay compute \
'read line; printf "%s\n" "$line"'A non-target host is dispatched directly to /usr/bin/ssh:
~/.local/bin/ssh-client-relay -G ssh-relayComplete the following setup before changing remote.SSH.path.
This configuration is for a VS Code client process running on Linux. The path
in remote.SSH.path is resolved by the client process. A Windows VS Code client
cannot execute /home/your-user/.local/bin/ssh-client-relay; it needs the
Windows client executable instead.
The relay name passed to install.sh must work with the client's normal SSH.
For example, the Linux client's ~/.ssh/config can contain:
Host ssh-relay
HostName relay.example.com
User relay-user
Port 22
IdentityFile ~/.ssh/id_ed25519Verify the outer connection independently:
/usr/bin/ssh ssh-relay true
/usr/bin/ssh -o BatchMode=yes ssh-relay trueThe second command should succeed without a password or interactive MFA prompt. If it does not, configure a key or a separate ControlMaster for the relay. Any authentication prompt on this outer connection will otherwise occur whenever VS Code starts a relayed connection.
On the relay machine, configure the final server in ~/.ssh/config. For
example, ssh-relay:~/.ssh/config needs an entry equivalent to:
Host compute.example.org compute
HostName compute.example.org
User remote-user
ControlMaster auto
ControlPath ~/.ssh/controlmasters/%r@%h:%p
ControlPersist 48hCreate the socket directory once:
ssh ssh-relay 'mkdir -p ~/.ssh/controlmasters && chmod 700 ~/.ssh/controlmasters'Then log in from the relay and complete any password or MFA authentication. An interactive session, service, or command such as the following can establish the master:
ssh -t ssh-relay 'ssh compute'After disconnecting that interactive session, confirm that the persistent master remains available:
ssh ssh-relay 'ssh -O check compute'The expected result contains Master running. The relay can connect without a
ControlMaster, but then every new VS Code connection may require the target's
normal authentication.
Run the installer on the Linux client:
./install.sh ssh-relay compute compute.example.orgBefore involving VS Code, all of these commands must work:
~/.local/bin/ssh-client-relay -O check compute
~/.local/bin/ssh-client-relay compute hostname
~/.local/bin/ssh-client-relay -G compute | grep -E '^(hostname|user|port) 'The first command should report the relay's master as running. The second should print the final server's hostname. The third should show the final server's effective SSH configuration as evaluated on the relay.
Also verify that an unrelated host still uses local SSH:
~/.local/bin/ssh-client-relay -G ssh-relayVS Code reads the client's SSH config to populate Remote-SSH: Connect to Host.
Add at least a host declaration to the Linux client's ~/.ssh/config:
Host compute
HostName compute.example.orgThe connection details used for the final hop still come from the relay's SSH configuration. This local entry primarily makes the target visible to VS Code and ensures that either the alias or canonical hostname triggers dispatch.
In the Linux VS Code user settings.json, set the absolute path:
"remote.SSH.path": "/home/your-user/.local/bin/ssh-client-relay",
"remote.SSH.useExecServer": trueAlthough remote.SSH.path is global, the wrapper relays only the configured
target alias or canonical hostname. Other hosts continue through
/usr/bin/ssh on the client.
If VS Code Settings Sync is shared with a Windows client, keep the executable path local to each platform:
"settingsSync.ignoredSettings": [
"remote.SSH.path"
]Merge remote.SSH.path into an existing settingsSync.ignoredSettings array
rather than creating a second array. The remote.SSH.useExecServer value can
be synced because true is appropriate on both platforms.
Reload VS Code, run Remote-SSH: Connect to Host, and select the configured
target alias. If connection fails, inspect View: Output and select
Remote - SSH; first rerun the commands in steps 2 through 4 outside VS Code to
identify whether the failed hop is client-to-relay or relay-to-target.
Complete these steps before changing remote.SSH.path.
Add the Linux relay to %USERPROFILE%\.ssh\config:
Host ssh-relay
HostName relay.example.com
User relay-user
Port 22
IdentityFile ~/.ssh/id_ed25519From PowerShell, verify both interactive and non-interactive access:
ssh.exe ssh-relay true
ssh.exe -o BatchMode=yes ssh-relay trueThe second command should complete without a password or MFA prompt. This is the outer connection. It is independent of the relay's ControlMaster for the final server.
Configure and establish the target ControlMaster on the Linux relay exactly as described in steps 3 and 4 of the Linux VS Code section. From PowerShell, the key verification is:
ssh.exe ssh-relay "ssh -O check compute"It should report Master running before VS Code is started.
In Windows PowerShell, from this repository:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\install-windows.ps1 `
ssh-relay compute compute.example.orgThe installer itself connects to the relay twice to deploy the helper. Complete any outer-hop authentication prompts during installation.
Run all checks from PowerShell:
& "$env:USERPROFILE\bin\ssh-client-relay.exe" -O check compute
& "$env:USERPROFILE\bin\ssh-client-relay.exe" compute hostname
& "$env:USERPROFILE\bin\ssh-client-relay.exe" -G compute
& "$env:USERPROFILE\bin\ssh-client-relay.exe" -G ssh-relayThe first three commands use the Linux relay. The last command demonstrates that an unrelated destination still goes directly through Windows OpenSSH.
Add the target alias to %USERPROFILE%\.ssh\config if it is not already
present:
Host compute
HostName compute.example.orgThis entry makes compute appear in Remote-SSH: Connect to Host. The final
connection settings and credentials still come from the Linux relay.
In the Windows VS Code user settings.json, set:
"remote.SSH.path": "C:\\Users\\your-user\\bin\\ssh-client-relay.exe",
"remote.SSH.useExecServer": trueReload VS Code and select compute from Remote-SSH: Connect to Host. The path
must point to the .exe, not the PowerShell installer or C# source file.
When the same Settings Sync account is used on Linux and Windows, also add
remote.SSH.path to settingsSync.ignoredSettings as shown in the Linux
section. Otherwise one platform can overwrite the other platform's executable
path. On failure, Remote SSH may silently fall back to ordinary ssh, which
causes the target to authenticate directly instead of using the relay.
Start by testing each layer outside VS Code:
/usr/bin/ssh -o BatchMode=yes ssh-relay true
ssh ssh-relay 'ssh -O check compute'
~/.local/bin/ssh-client-relay -O check compute
~/.local/bin/ssh-client-relay compute trueFor a Windows client, run the equivalent checks in PowerShell with ssh.exe
and %USERPROFILE%\bin\ssh-client-relay.exe.
Common failure signatures:
- A password or MFA prompt for the relay account means the outer client-to-relay connection is not non-interactive.
- A password or MFA prompt for the target means the relay-to-target ControlMaster is absent, expired, or unable to open another channel.
- A log saying the configured SSH path is not a valid binary, followed by a
plain
sshcommand, meansremote.SSH.pathis wrong for the client OS. - Repeated
Installation already in progress, followed byExhaustedRetries, can come from the legacy VS Code bootstrap colliding with a legitimately running server. Keepremote.SSH.useExecServerset totrue. Do not delete a remote lock until confirming no live VS Code server owns it.
The relay's own version is printed with --version. VS Code probes SSH
runtimes with -V; the wrapper intentionally passes that non-target invocation
to the underlying OpenSSH client, so seeing an OpenSSH version there is normal.
The Linux installer also provides ~/.local/bin/ssh-client-relay-sshfs. It
runs SSHFS with ssh-client-relay as the SSH transport, so the SFTP process is
created on the Linux relay and can reuse the relay's target ControlMaster.
client FUSE mount -> relayed SSH stream -> relay ControlMaster -> target SFTP
After installing and verifying the normal relay connection, mount a remote directory with:
mkdir -p ~/remote-mount
ssh-client-relay-sshfs \
-o reconnect,ServerAliveInterval=15,ServerAliveCountMax=3 \
compute.example.org:/remote/path ~/remote-mountRun it in the foreground when a service manager owns the process:
ssh-client-relay-sshfs -f \
-o reconnect,ServerAliveInterval=15,ServerAliveCountMax=3 \
compute.example.org:/remote/path ~/remote-mountUnmount with the platform's normal FUSE command:
fusermount3 -u ~/remote-mountThe helper accepts normal SSHFS arguments and adds only the ssh_command
option. Set SSH_CLIENT_RELAY_BIN to override the installed relay executable.
The target alias or hostname must match the target configured by install.sh.
Before mounting, test a real target session rather than only checking the ControlMaster process:
ssh-client-relay compute.example.org trueEach active SSHFS mount consumes a target SFTP subsystem session on the relay's ControlMaster. A mount fails if the relay master is absent, saturated, or no longer accepts new channels.
Licensed under the Apache License 2.0.
Run the local Linux tests with:
tests/test-linux.shRun the Windows tests from PowerShell with:
tests\test-windows.ps1GitHub Actions runs both suites on every push and pull request. The tests use fake local SSH transports and require no credentials or network hosts.
Show the installed client version with:
ssh-client-relay --versionSee CHANGELOG.md and SECURITY.md for release and vulnerability-reporting information. Contributions are described in CONTRIBUTING.md.