Bug type
Behavior bug (incorrect output/state without crash)
Beta release blocker
No
Summary
RemotePortTunnel spawns ssh -N -L <local>:127.0.0.1:18789 child processes, but cleanup only happens via terminate()/deinit in the owning app process. When the app crashes, is force-killed, or a dev build is stopped from Xcode, the ssh child reparents to launchd and keeps the forward alive indefinitely. PortGuardian.sweep never reaps them: it only inspects the canonical gateway port (GatewayEnvironment.gatewayPort()), and in remote mode it explicitly refuses to kill listeners on that port. The recorded pids in port-guard.json are written but never used to reap orphans.
Live evidence on one dev machine: 10 orphaned ssh -N -L tunnels to the same remote host (oldest from 2 days prior), including one squatting the preferred local port 18789. Because the preferred port is taken, every new tunnel falls back to a random ephemeral port (allowRandomLocalPort: true), which in turn makes the Dashboard WebView's baked-in endpoint go stale on every tunnel restart (see companion issue).
Steps to reproduce
- Remote (SSH) mode; let the app create a control tunnel.
kill -9 the OpenClaw app (or stop a dev build from Xcode).
ps aux | grep 'ssh -o BatchMode' → tunnel still running, parent launchd.
- Relaunch the app repeatedly; tunnels accumulate, and the preferred local port stays occupied by a dead session's tunnel.
Expected behavior
On startup (and on PortGuardian.sweep), reap recorded tunnel processes that no longer have a living owner — e.g. kill pids from port-guard.json whose command matches the app's ssh tunnel signature and whose parent is launchd (orphaned). Tunnels owned by other live OpenClaw instances must be left alone.
Actual behavior
Orphans accumulate forever, hold SSH connections to the remote host, and permanently occupy the preferred local port, forcing ephemeral-port drift.
OpenClaw version
main @ 5fdaa04
Operating system
macOS 26.5 (Apple Silicon)
Install method
git checkout (dev build)
Model
n/a
Provider / routing chain
n/a
Additional provider/model setup details
n/a
Logs
ps snapshot showed 10 ssh -o BatchMode=yes ... -N -L <port>:127.0.0.1:18789 -- steipete@<host> processes with ppid 1; only one belonged to the running app.
Screenshots, recordings, and evidence
- Spawn + record:
apps/macos/Sources/OpenClaw/RemotePortTunnel.swift (create, PortGuardian.shared.record).
- Sweep never covers tunnel ports and skips remote mode:
apps/macos/Sources/OpenClaw/PortGuardian.swift (sweep).
- Random-port fallback when preferred port is occupied:
RemotePortTunnel.findPort / RemoteTunnelManager.ensureControlTunnel.
Impact and severity
Resource leak (ssh processes + remote connections) and root cause of tunnel-port instability that breaks open Dashboard windows in remote mode (#100476; web-side UX: #100475). Medium.
Additional information
Note for the fix: sweep must not assume "any ssh -L to gateway port is ours to kill" — match against recorded pids from port-guard.json plus orphaned-parent check, since several OpenClaw instances (prod + dev) can legitimately run tunnels concurrently on one machine.
Bug type
Behavior bug (incorrect output/state without crash)
Beta release blocker
No
Summary
RemotePortTunnelspawnsssh -N -L <local>:127.0.0.1:18789child processes, but cleanup only happens viaterminate()/deinitin the owning app process. When the app crashes, is force-killed, or a dev build is stopped from Xcode, the ssh child reparents to launchd and keeps the forward alive indefinitely.PortGuardian.sweepnever reaps them: it only inspects the canonical gateway port (GatewayEnvironment.gatewayPort()), and in remote mode it explicitly refuses to kill listeners on that port. The recorded pids inport-guard.jsonare written but never used to reap orphans.Live evidence on one dev machine: 10 orphaned
ssh -N -Ltunnels to the same remote host (oldest from 2 days prior), including one squatting the preferred local port 18789. Because the preferred port is taken, every new tunnel falls back to a random ephemeral port (allowRandomLocalPort: true), which in turn makes the Dashboard WebView's baked-in endpoint go stale on every tunnel restart (see companion issue).Steps to reproduce
kill -9the OpenClaw app (or stop a dev build from Xcode).ps aux | grep 'ssh -o BatchMode'→ tunnel still running, parent launchd.Expected behavior
On startup (and on
PortGuardian.sweep), reap recorded tunnel processes that no longer have a living owner — e.g. kill pids fromport-guard.jsonwhose command matches the app's ssh tunnel signature and whose parent is launchd (orphaned). Tunnels owned by other live OpenClaw instances must be left alone.Actual behavior
Orphans accumulate forever, hold SSH connections to the remote host, and permanently occupy the preferred local port, forcing ephemeral-port drift.
OpenClaw version
main @ 5fdaa04
Operating system
macOS 26.5 (Apple Silicon)
Install method
git checkout (dev build)
Model
n/a
Provider / routing chain
n/a
Additional provider/model setup details
n/a
Logs
pssnapshot showed 10ssh -o BatchMode=yes ... -N -L <port>:127.0.0.1:18789 -- steipete@<host>processes with ppid 1; only one belonged to the running app.Screenshots, recordings, and evidence
apps/macos/Sources/OpenClaw/RemotePortTunnel.swift(create,PortGuardian.shared.record).apps/macos/Sources/OpenClaw/PortGuardian.swift(sweep).RemotePortTunnel.findPort/RemoteTunnelManager.ensureControlTunnel.Impact and severity
Resource leak (ssh processes + remote connections) and root cause of tunnel-port instability that breaks open Dashboard windows in remote mode (#100476; web-side UX: #100475). Medium.
Additional information
Note for the fix: sweep must not assume "any ssh -L to gateway port is ours to kill" — match against recorded pids from
port-guard.jsonplus orphaned-parent check, since several OpenClaw instances (prod + dev) can legitimately run tunnels concurrently on one machine.