wavetermdev/waveterm · error

failed to start command

Error message

failed to start command: %w

What it means

After building the full command string, Start() calls session.Start(fullCmd) on the golang.org/x/crypto/ssh session. If the remote side refuses to begin execution of the command (channel/request error at the SSH protocol level), the error is wrapped as 'failed to start command'. The session channel exists but the exec request failed.

Solutions

  1. Check the wrapped error: 'connection lost' style errors mean reconnect and retry; request-failed means inspect remote sshd restrictions.
  2. Verify the shell/executable named in the CommandSpec exists on the remote host (which bash).
  3. Test the same command manually over plain ssh to rule out forced-command/Match restrictions.
  4. Add keepalives to detect dead connections before Start.

Example fix

// before
err := ctrl.Start() // opaque ssh error, controller unusable
// after
if err := ctrl.Start(); err != nil {
    if strings.Contains(err.Error(), "connection lost") || strings.Contains(err.Error(), "EOF") {
        client = reconnectSSH(cfg)
        ctrl, err = genconn.MakeSSHCmdClient(client, spec)
        if err == nil { err = ctrl.Start() }
    }
    if err != nil { return err }
}
Defensive patterns

Strategy: retry

Validate before calling

func remoteHasShell(client *ssh.Client, shell string) bool {
    out, err := client.NewSession(); if err != nil { return false }
    defer out.Close()
    err = out.Run("command -v " + shell)
    return err == nil
}

Try / catch

if err := ctrl.Start(); err != nil {
    if isConnError(err) { // EOF, "connection lost", "broken pipe"
        client = reconnectSSH(cfg)
        ctrl, _ = genconn.MakeSSHCmdClient(client, spec)
        return ctrl.Start()
    }
    return fmt.Errorf("remote refused exec (check sshd restrictions / shell path): %w", err)
}

Prevention

When it happens

Trigger: Calling Start() when the underlying SSH connection broke between session creation and Start, the remote sshd rejects the exec request, or the requested executable/shell cannot be invoked on the remote side.

Common situations: Network drop right after session creation; remote shell path in CommandSpec does not exist on the host (e.g. zsh not installed); sshd forced-command restrictions rejecting arbitrary exec; server overloaded/out of PTYs.

Related errors


AI-assisted analysis of wavetermdev/waveterm@a4447c1563 (2026-09-01). Data as JSON: /api/errors/f88ed17a464622a0. Report an issue: GitHub.

Appendix: source

Thrown at pkg/genconn/ssh-impl.go:78

func (s *SSHProcessController) Start() error {
	s.lock.Lock()
	defer s.lock.Unlock()

	if s.started {
		return fmt.Errorf("command already started")
	}

	fullCmd, err := BuildShellCommand(s.cmdSpec)
	if err != nil {
		return fmt.Errorf("failed to build shell command: %w", err)
	}
	// if stdout/stderr weren't piped, then session.stdout/stderr will be nil
	// and the library guarantees that the outputs will be attached to io.Discard
	// if stdin hasn't been piped, then session.stdin will be nil
	// and the libary guarantees that it will be attached to an empty bytes.Buffer, which will produce an immediate EOF
	// tl;dr we don't need to worry about hanging beause of long input or explicitly closing stdin
	if err := s.session.Start(fullCmd); err != nil {
		return fmt.Errorf("failed to start command: %w", err)
	}
	s.started = true
	return nil
}

// Wait waits for the command to complete
func (s *SSHProcessController) Wait() error {
	s.once.Do(func() {
		s.waitErr = s.session.Wait()
	})
	return s.waitErr
}

// Kill terminates the command
func (s *SSHProcessController) Kill() {
	s.lock.Lock()
	defer s.lock.Unlock()

View on GitHub (pinned to a4447c1563)