hashicorp/nomad · error

not a terminal

Error message

not a terminal

What it means

The nomad alloc exec command puts the local terminal in raw mode so that keystrokes like Ctrl+C are forwarded to the remote process. This error is thrown by setRawTerminal when the stream passed to it (os.Stdin) is not attached to a TTY, detected via term.GetFdInfo. Without a real terminal, raw-mode capture and interactive forwarding are impossible.

Source

Thrown at command/alloc_exec.go:316

	signalCh := make(chan os.Signal, 1)
	signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
	go func() {
		for range signalCh {
			cancelFn()
		}
	}()

	return client.Allocations().Exec(ctx,
		alloc, task, tty, command, stdin, stdout, stderr, sizeCh, nil)
}

// setRawTerminal sets the stream terminal in raw mode, so process captures
// Ctrl+C and other commands to forward to remote process.
// It returns a cleanup function that restores terminal to original mode.
func setRawTerminal(stream any) (cleanup func(), err error) {
	fd, isTerminal := term.GetFdInfo(stream)
	if !isTerminal {
		return nil, errors.New("not a terminal")
	}

	state, err := term.SetRawTerminal(fd)
	if err != nil {
		return nil, err
	}

	return func() { term.RestoreTerminal(fd, state) }, nil
}

// setRawTerminalOutput sets the output stream in Windows to raw mode,
// so it disables LF -> CRLF translation.
// It's basically a no-op on unix.
func setRawTerminalOutput(stream any) (cleanup func(), err error) {
	fd, isTerminal := term.GetFdInfo(stream)
	if !isTerminal {
		return nil, errors.New("not a terminal")
	}

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Allocate a TTY: use `ssh -t` when connecting remotely, or `docker run -it` / `docker exec -it` when inside a container.
  2. If stdin redirection is intended, add `-i` (no stdin) so the command does not try to attach stdin and enter raw mode.
  3. Run the command from an interactive terminal session instead of a script.

Example fix

// before
$ cat input.txt | nomad alloc exec -job web /bin/sh
// after
$ nomad alloc exec -i -job web /bin/sh   # or run from a real TTY with an interactive shell
Defensive patterns

Strategy: validation

Validate before calling

if term.IsTerminal(int(os.Stdin.Fd())) {
    // safe to run interactive exec
    runAllocExec()
} else {
    runAllocExecWithNoStdin() // add -i
}

Type guard

func isTTY(f *os.File) bool { return term.IsTerminal(int(f.Fd())) }

Try / catch

if err := runAllocExec(); err != nil && strings.Contains(err.Error(), "not a terminal") {
    // fall back to non-interactive mode with -i
}

Prevention

When it happens

Trigger: Running `nomad alloc exec` with stdin redirected (e.g. `nomad alloc exec ... < file`), piped from another command, or run from a script/CI runner/cron job where stdin is not a TTY.

Common situations: CI pipelines (Jenkins, GitHub Actions), SSH without -t, piping stdin from a heredoc or echo, running inside a non-interactive shell or docker exec without -t.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/69634e2794058698. Report an issue: GitHub.