Discover services reachable through IPv4 loopback on a Linux SSH host and keep
selected ports—or ports whose process working directory matches a configured
glob—available at localhost through system OpenSSH. Remembered forwards may
use a different preferred local port and choose whether to fall back when it
is busy. Automatic forwards always allow temporary fallback.
ssh-forward is a small SSH tunnel manager for remote development.
Authentication and connection options stay in OpenSSH and
~/.ssh/config.
Remote development servers often start HTTP applications on unpredictable or
short-lived ports. A manual ssh -L works, but you have to discover the port,
start the tunnel, and recreate it after the SSH connection changes.
ssh-forward shows remote listeners reachable at 127.0.0.1, remembers the
ports and working-directory globs you choose, and keeps the required local SSH
port forwards running in the background. It does not install a remote agent or
store SSH credentials.
Forward short-lived development servers whose working directories are anywhere under a remote workspace, without discovering and adding each port:
ssh-forward add --pwd '/home/me/Workspace/**'When a matching remote process starts listening, its port becomes available on
the same port at localhost, or the next available port if that port is busy.
When the listener stops, the automatic SSH forward disappears. This works well
for development servers, preview tools, notebooks, and OAuth callback servers
that use temporary ports.
Measured with v0.1.0 on an Apple M1 Pro running macOS 26.6.2:
| Metric | Result |
|---|---|
| Release archive, across four supported targets | 3.75–4.17 MB |
| Unpacked binary | 9.4–10.2 MB |
| Idle Manager with discovery only | 18.2 MiB total RSS |
| Idle Manager with one active forward | 23.2 MiB total RSS |
| Idle CPU, ten one-second samples | 0.0% |
Warm ssh-forward --version startup |
7.4 ms average |
The v0.1.0 runtime totals include the Manager and the system OpenSSH process layout used by that release. Current Managers use one product-owned OpenSSH master connection and add or cancel forwards through its control socket, so the number of SSH transports no longer grows with the number of ports. RSS can count shared pages more than once. At the 256-port observation limit, a complete Manager status snapshot takes about 28–30 µs with two allocations, and parsing a complete scanner frame takes about 44–49 µs.
These numbers are a reproducible baseline rather than a performance guarantee. Run the benchmarks with:
cd cli
go test -run '^$' -bench . -benchmem ./internal/core ./internal/opensshThe only runtime dependency is a system OpenSSH client. The local machine may run macOS or Linux; the remote Development Host must run Linux.
With Homebrew:
brew install wangnan0916/ssh-forward/ssh-forwardRelease archives for macOS and Linux on Apple Silicon/ARM64 and AMD64 are available from GitHub Releases.
Or build from source with Go 1.26 or newer:
git clone https://github.com/wangnan0916/ssh-forward.git
cd ssh-forward/cli
go build -o ssh-forward ./cmd/ssh-forwardUse a literal Host alias from your SSH config:
Host my-dev
HostName dev.example.com
User meThen choose the host and remember the ports you want locally:
ssh-forward default my-dev
ssh-forward status # see remote loopback listeners
ssh-forward add 5173 # prefer localhost:5173; temporarily fall back if busy
ssh-forward add 8443 --local 18443 # require remote 8443 on localhost:18443
ssh-forward add --pwd '/home/me/Workspace/**' # forward matching live services
ssh-forward status --watch # follow changes
ssh-forward remove 5173
ssh-forward remove --pwd '/home/me/Workspace/**'The first command that needs a connection automatically installs and starts a user-scoped background manager. Later commands reuse it. After an upgrade, the next command automatically replaces an older Manager.
ssh-forward add PORT [--local PORT]
ssh-forward add --pwd GLOB
ssh-forward remove PORT
ssh-forward remove --pwd GLOB
ssh-forward status [--json] [--watch]
ssh-forward doctor [--json]
ssh-forward host [--json]
ssh-forward default [ALIAS]
ssh-forward uninstall
Global options are --host ALIAS and --ssh-config PATH. Set
SSH_FORWARD_CONFIG_DIR to move product state.
- A fixed shell script runs through
ssh HOST sh -sand reads Linux procfs listener state. - It reports at most 256 TCP listeners reachable at
127.0.0.1, including same-user IPv4 wildcard listeners and dual-stack IPv6 wildcard listeners. Restricting wildcard discovery to the SSH user avoids listing system-wide services such as the SSH daemon itself. IPv6-only listeners stay hidden because the forwarding target cannot reach them. Executable names and working directories are collected on a best-effort basis whenssand the relevant procfs links are available. No remote agent is installed. - The Manager owns one product-private OpenSSH master connection and uses OpenSSH control commands to add and cancel each desired remote-to-local forward. The local port stays available while the remote process restarts; individual connections fail until the remote listener returns. Stopping one Forward does not disturb the shared connection or other ports.
- Absolute working-directory globs create Automatic Forwards for matching
Remote Listeners.
*matches within one path segment and**crosses path segments. When a listener disappears or stops matching, its Automatic Forward stops. Quote globs so the local shell does not expand them. - Remembered Forwards created without
--localand Automatic Forwards try up to 20 higher local ports when the preferred port is busy. The actual port appears in status but is temporary and is never written to configuration. Explicit--localmappings are strict by default. - HTTP over a user-only Unix socket lets later CLI calls read Manager status.
status --watchpolls that status.
The OS user service manager (launchd on macOS, the detected init system on Linux) owns process startup, restart, and logs. Installation and startup happen automatically when a command first needs the Manager.
Run ssh-forward doctor for a read-only check of config files, OpenSSH, Host
selection, Manager health, Forward failures, and a real remote listener scan.
It does not install, restart, or change the Manager. Use --json for
automation.
Use add REMOTE --local LOCAL when a stable local address is required. The
command creates a strict mapping: if another process owns that port, status
reports the conflict and retries later. Remembered preferred ports for one
Host must be distinct.
All persistent intent is in one config.jsonc:
allow_fallback is authoritative whether or not the remote and local ports
differ. add REMOTE enables it, while add REMOTE --local LOCAL leaves it
disabled. A manually edited schema-4 entry may explicitly choose either
policy.
Commands send remembered-forward and working-directory-rule changes to the running Manager, which reconciles only the affected forwards. Unchanged forwards stay connected. A selected-host, protocol, or binary-version change still replaces the Manager. Schema 1–3 files remain readable; during migration, legacy same-port mappings gain temporary fallback and legacy custom mappings remain strict. The file upgrades to schema 4 on the next write. Runtime observations, temporary actual ports, and process IDs are not persisted.
Default directories:
- macOS:
~/Library/Application Support/ssh-forward/ - Linux:
$XDG_CONFIG_HOME/ssh-forward/or~/.config/ssh-forward/
Homebrew upgrades the binary normally:
brew upgrade ssh-forwardRun the product uninstall command before removing the binary. It removes the
background service but keeps config.jsonc for a later reinstall:
ssh-forward uninstall
brew uninstall ssh-forwardDelete the configuration directory separately if you also want to forget the selected Host and ports.
- Linux Development Hosts only
- macOS and Linux local clients only
- one active SSH host per Manager
- TCP listeners reachable through remote
127.0.0.1; IPv6-only listeners are excluded - Automatic Forwards require best-effort process working-directory metadata; listeners without that metadata cannot match a rule
For CLI-only changes, build and run the latest code against the installed Manager without replacing the background service:
./scripts/dev statusUse full mode when Manager behavior also changed. It temporarily runs the development Manager and restores the installed one when the command exits:
./scripts/dev --full statusBoth modes build an ignored binary at .tmp/dev/ssh-forward. By default the
baseline binary is the ssh-forward found on PATH; set
SSH_FORWARD_DEV_BASELINE to select another installed binary.
Run the complete local check suite with:
./scripts/checkThe disposable Linux/OpenSSH integration test requires Docker:
./scripts/test-integrationSee ARCHITECTURE.md, CONTRIBUTING.md, and SECURITY.md. Ask usage questions or report bugs through GitHub Issues.
{ "schema_version": 4, "default_host": "my-dev", "remembered_forwards": { "my-dev": [ {"remote_port": 5173, "local_port": 5173, "allow_fallback": true}, {"remote_port": 8443, "local_port": 18443} ] }, "working_directory_rules": { "my-dev": ["/home/me/Workspace/**"] } }