argoproj/argo-workflows · error

cannot convert stdin to os.File, it was %T

Error message

cannot convert stdin to os.File, it was %T

What it means

StartCommand uses a pty to run a command with a TTY when stdin should be interactive. pty.Start requires cmd.Stdin to be an *os.File (a real file descriptor); if the Stdin field holds any other io.Reader implementation, it cannot be attached to the pty and this error is returned. The code comment notes it 'should never happen when stdin is a tty', so it indicates a non-file stdin was configured.

Source

Thrown at workflow/executor/osspecific/command_unix.go:45

	}

	cmd.SysProcAttr = &syscall.SysProcAttr{}

	if !isTerminal(cmd.Stdin) {
		// avoid the error "Inappropriate ioctl for device" when
		// running in tty
		//
		// pty.Start uses setsid internally, which makes the process
		// the group leader already
		Setpgid(cmd.SysProcAttr)

		return simpleStart(cmd)
	}

	stdin, ok := cmd.Stdin.(*os.File)
	if !ok {
		// should never happen when stdin is a tty
		return nil, fmt.Errorf("cannot convert stdin to os.File, it was %T", cmd.Stdin)
	}

	stdout := cmd.Stdout
	stderr := cmd.Stderr

	// pty.Start will not assign these to the pty unless they are nil
	cmd.Stdin = nil
	cmd.Stdout = nil
	cmd.Stderr = nil

	ptmx, err := pty.Start(cmd)
	if err != nil {
		return nil, err
	}

	// Handle pty size
	sigWinchCh := make(chan os.Signal, 1)
	signal.Notify(sigWinchCh, syscall.SIGWINCH)

View on GitHub (pinned to 35bff19146)

Solutions

  1. Set cmd.Stdin to an *os.File — open /dev/null or the real terminal file: `f, _ := os.Open("/dev/null"); cmd.Stdin = f`
  2. If stdin is a pipe or wrapped reader, use simpleStart (the non-pty path) instead of the pty path
  3. If you need to feed in-memory data over a pty, write it to an os.Pipe and pass the *os.File read end as cmd.Stdin
  4. Remove intermediate wrappers (bufio.Reader, io.MultiReader) and pass the raw file

Example fix

// before
cmd.Stdin = bytes.NewBufferString(input)
// after
r, w, _ := os.Pipe()
go func() { w.WriteString(input); w.Close() }()
cmd.Stdin = r // *os.File
Defensive patterns

Strategy: type-guard

Validate before calling

if _, ok := cmd.Stdin.(*os.File); !ok {
    return simpleStart(cmd) // or replace Stdin with an *os.File
}

Type guard

func isFileStdin(cmd *exec.Cmd) bool {
    _, ok := cmd.Stdin.(*os.File)
    return ok
}

Try / catch

if err := StartCommand(ctx, cmd); err != nil && strings.Contains(err.Error(), "cannot convert stdin to os.File") {
    return simpleStart(cmd) // fallback to non-pty start
}

Prevention

When it happens

Trigger: Calling StartCommand with a cmd whose Stdin was set to something other than *os.File — e.g. bytes.Buffer, os.Pipe read end is fine but a wrapped reader, strings.Reader, net.Conn, or nil-cast generic io.Reader is not.

Common situations: Custom executor code or tests constructing exec.Cmd with an in-memory reader and requesting a TTY/interactive start; code paths that wrap stdin in a bufio.Reader or io.LimitReader; running where the caller assumed pty.Start would fall back gracefully.

Related errors


AI-assisted analysis of argoproj/argo-workflows@35bff19146 (2026-09-03). Data as JSON: /api/errors/53b4fabd07948f47. Report an issue: GitHub.