Repository navigation
[OpenShell] 'openshell forward list' reports STATUS: dead but there is no supervisor, auto-restart, or alerting — silently unreachable dashboards #874
Description
Description
openshell forward start <port> <sandbox> spawns an SSH-backed port-forward from the host to a port inside the sandbox pod. When the underlying SSH process dies (e.g. gateway pod restart, kubectl delete pod, transient network blip), the forward becomes unusable but openshell keeps the entry in its internal state and openshell forward list simply marks the STATUS as dead:
SANDBOX BIND PORT PID STATUS
my-assistant 127.0.0.1 18789 1803950 dead
No log message is emitted. No supervisor kicks in. No operator-visible warning appears. The dashboard at http://127.0.0.1:18789/ silently returns connection-refused. An operator tunnelling from a remote machine just sees "Unable to connect" with no indication of the cause.
The status field is already being computed — the same list command that prints dead has the information needed to act — but no code path consumes it.
Environment
- OpenShell v0.0.26 (bundled with NemoClaw v0.0.17 and v0.0.18)
- Host: Linux (x86_64); forward is an ssh process spawned by
openshell forward start - Trigger: anything that breaks the underlying SSH tunnel — most commonly gateway pod crash or sandbox pod restart
Reproduction
- Start a managed forward:
openshell forward start 18789 my-assistant -g nemoclaw --background openshell forward list # STATUS: running - Break the underlying transport — easiest is
kubectl delete pod -n openshell my-assistanton the cluster, which terminates the sandbox the SSH session is bound to. - Wait ~30 seconds for the new pod to be Ready.
openshell forward listnow showsSTATUS: dead. The forward process is gone. Port 18789 no longer listens on the host.- Nothing else happens. The
deadstatus is the only signal. The dashboard remains unreachable until the operator manually runsopenshell forward stop 18789and thenopenshell forward start 18789 <sandbox> --backgroundagain.
Expected behaviour
One of:
- Supervisor default on:
openshell forward startshould, by default, keep the forward alive — if the underlying ssh dies, the supervisor restarts it. Expose--no-auto-restartfor operators who want the current behaviour. - Explicit restart command: at minimum, add
openshell forward restart <port>as a first-class command that's idempotent (no-op if already running, restart if dead). Currently the only recovery isstopthenstart --backgroundmanually. - Alerting: emit a warning to the gateway logs whenever a forward is observed dead on
list, or expose a way to subscribe to status changes.
Observability-without-remediation is a sharp edge for any production-adjacent deployment.
Actual behaviour
openshell forward --help:
start Start forwarding a local port to a sandbox
stop Stop a background port forward
list List active port forwards
No restart, no supervise, no monitor, no reconcile. Three verbs, none of which act on the dead status.
Additional sharp edge discovered
When an operator runs openshell forward start <port> <sandbox> without --background, the command blocks on a foreground SSH tunnel. When that foreground session's ssh connection drops (e.g. the operator logs out of the host), the tunnel dies with it — but not cleanly. The child ssh process may persist, bound to the port, untracked by openshell. openshell forward list then reports "No active forwards" while ss -tlnp shows a process still holding the port. openshell forward stop 18789 says "No active forward found." Operators are left with an orphan tunnel that serves traffic but cannot be managed by openshell.
Fix suggestion: warn or error when openshell forward start is invoked without --background over a non-interactive ssh session (e.g., detect SSH_CONNECTION env + non-TTY stdin).
Source citation
crates/openshell-core/src/forward.rs (full implementation; ~150 lines). Functions defined:
forward_pid_dir()— where PID files liveforward_pid_path(name, port)— compute PID file pathwrite_forward_pid(name, port, pid, sandbox_id, bind_addr)— record a forwardfind_ssh_forward_pid(sandbox_id, port)— fall back to pgrep to find the actual SSH PIDread_forward_pid(name, port)— parse PID file backpid_is_alive(pid)—kill -0 <pid>checkpid_matches_forward(pid, port, sandbox_id)— validate the PID still matches an SSH command line
There is no restart function, no supervisor, no retry, no healthcheck loop. pid_is_alive is how the STATUS column is computed — it's a point-in-time check, nothing more. When pid_is_alive(pid) returns false, the CLI renders dead and does nothing else.
That means the dead status in openshell forward list is observation without remediation by design. Fixing this bug means adding new code paths, not changing existing ones.
Impact
- Silent dashboard unreachability after routine operations (pod restart, CR edit, rolling update). This is the "disappearing dashboard" symptom several users have reported.
- Operator mental model mismatch:
openshell forward listlooks like a managed service list, but the entries are not actually being managed. - Related: contributes to a larger downstream NemoClaw bug where
pod restart leaves gateway and port-forward dead, recovery hidden inside connect— the recovery is only reachable as a side effect of the interactivenemoclaw connectcommand, because there's no dedicated supervisor.
Suggested fix (preference order)
- Short term: add
openshell forward restart <port>as an idempotent command. No-op if alive, restart if dead. Trivial to implement — all the PID-detection plumbing already exists. - Medium term: have
openshell forward listauto-restart anydeadentries it observes (with a--no-auto-restartopt-out), or emit a log warning on every observed dead forward. - Medium term: warn or error when
openshell forward startis invoked without--backgroundover a non-interactive ssh session. - Longer term: a proper supervisor process that owns all forwards and keeps them alive.
Evidence
- Live repro 2026-04-17 —
openshell forward listshowedSTATUS: deadafter akubectl delete podpod recovery. Dashboard at127.0.0.1:18789returned connection-refused until the operator manually ranopenshell forward stop 18789 && openshell forward start 18789 <sandbox> --background. openshell forward --helpoutput (v0.0.26, unchanged in NemoClaw v0.0.18 install).ss -tlnp | grep 18789during the orphan-ssh state showedssh pid=2821709bound to127.0.0.1:18789, untracked byopenshell forward list.- Source review of
crates/openshell-core/src/forward.rsconfirms no supervisor code exists.
Happy to attach a nemoclaw debug --output <file>.tgz capture if useful.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
johntmyers commentedon Apr 17, 2026
Thanks for sharing. I'll defer to @pimlock but we are actively working on a more stable process to support connecting to running services in sandboxes that does not rely on port forwarding.
unco3 commentedon Apr 24, 2026
Same failure mode on v0.0.26 for a Tailscale-served OpenClaw webchat UI (forward target: port 18789 in a sandbox). Downstream sees
https://<host>/→ 502 and the only signal isopenshell forward listshowingSTATUS: dead.Mitigating with a 60-second polling systemd user service that calls
openshell forward stop+forward start -dwhenever the STATUS column (ANSI-stripped) is anything other thanrunning. Stable enough to keep an always-on webchat unblocked.Question for migration planning: @johntmyers mentioned earlier in this thread that OpenShell is actively working on a more stable replacement that doesn't rely on port forwarding, and #867 (supervisor-initiated SSH connect + exec over gRPC-multiplexed relay) looks like the foundation. Is that new transport planned to cover host-bound HTTP/WebSocket clients too — e.g., a browser on the host hitting
http://127.0.0.1:18789, or a Tailscale-served dashboard proxying to the same local port — or is its scope focused onsshandexecflows?If the new transport will cover web-UI use cases, I'd rather migrate when it lands than write a supervisor in
forward.rs. If not, supervisor default-on (proposal #1 in the original issue) seems warranted for web-UI workloads. Happy to test any branch in flight.perlowja commentedon Apr 28, 2026
Issue understanding
My read: the registry can retain an "active" forward row after the underlying SSH process has died.
forward listhas enough signal to printdead, but the only recovery path today is operator choreography: notice the stale row, runopenshell forward stop <port> <sandbox>, then re-runforward start ... --background(or wrap that in a polling service). That leaves host-bound dashboards/web UIs silently broken even though OpenShell has the state needed to make the next step obvious.Implementation offer
I can put up a small CLI/core PR that keeps
openshell forward listread-only by default, but makes stale forwards actionable:health: alive | dead | unknown(plain table column and JSON field).--prune-deadfor idempotent registry cleanup; it is a no-op when there is nothing dead.--restart-deadto re-establish dead forwards via the existing runtime/forwarding path.{ "schema_version": 1, "forwards": [...] }.I recognize the runtime layer owns the long-term connection model, especially if #867 supersedes SSH forwarding for some flows. This proposal is scoped as an operator-facing bridge for existing
forwardusers rather than trying to preempt that ownership.Implementation sketch
Want me to open a PR against
main? Happy to scope tighter (just health column first, prune/restart in a follow-up) if you prefer.github-actions commentedon Aug 30, 2026
This issue has had no activity for 14 days and is now marked stale. It may be closed in 7 days if there is no further activity. Comment or remove the state:stale label to keep it open.