wavetermdev/waveterm · error

failed to build shell command: %w

Error message

failed to build shell command: %w

What it means

SSHProcessController.Start builds the final shell command string from the stored CommandSpec via BuildShellCommand before handing it to the SSH session. If the CommandSpec is structurally invalid (BuildShellCommand returns an error), Start wraps and returns it as 'failed to build shell command'. The command never reaches the remote host.

Source

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

		lock:    &sync.Mutex{},
		once:    &sync.Once{},
		cmdSpec: cmdSpec,
		session: session,
	}, nil
}

// Start begins execution of the command
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()
	})

View on GitHub (pinned to a4447c1563)

Solutions

  1. Inspect the wrapped error from BuildShellCommand to see which spec field is invalid.
  2. Validate the CommandSpec before creating the controller (non-empty command, known shell).
  3. Fix the spec construction site (config file, JSON deserialization, or literal).

Example fix

// before
spec := genconn.CommandSpec{Shell: "powershell"} // on a linux remote
ctrl, _ := sshShellClient.MakeProcessController(spec)
err := ctrl.Start() // failed to build shell command
// after
spec := genconn.CommandSpec{Command: "ls -la", Shell: "bash"}
ctrl, err := sshShellClient.MakeProcessController(spec)
if err != nil { return err }
if err := ctrl.Start(); err != nil { return err }
Defensive patterns

Strategy: validation

Validate before calling

func validSpec(s genconn.CommandSpec) error {
    if s.Command == "" { return errors.New("CommandSpec.Command is empty") }
    switch s.Shell { case "", "bash", "sh", "zsh", "cmd", "powershell": return nil
    default: return fmt.Errorf("unsupported shell %q", s.Shell) }
}
// call before MakeSSHCmdClient/Start
if err := validSpec(spec); err != nil { return err }

Try / catch

if err := ctrl.Start(); err != nil {
    var buildErr error
    errors.As(err, &buildErr) // log full wrapped chain
    return fmt.Errorf("start failed (check CommandSpec): %w", err)
}

Prevention

When it happens

Trigger: Calling Start() on an SSHProcessController whose CommandSpec cannot be rendered by BuildShellCommand — e.g. an invalid command spec configuration passed to MakeSSHCmdClient/MakeProcessController (bad shell type, empty/invalid command field).

Common situations: Passing a CommandSpec with an unsupported shell value; constructing a spec programmatically with an empty Command; a spec deserialized from config/JSON missing required fields.

Related errors


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