FiloSottile/age · error

standard input is not a terminal, and /dev/tty is not availa

Error message

standard input is not a terminal, and /dev/tty is not available: %v

What it means

term.WithTerminal needs a terminal for interactive input. It first tries os.Stdin, then falls back to opening /dev/tty. This error means stdin is not a terminal AND /dev/tty could not be opened (err is the open failure). age's interactive prompts (secrets, confirmations) cannot proceed without a TTY, so the operation fails instead of reading from a non-interactive source.

Source

Thrown at internal/term/term.go:71

	if runtime.GOOS == "windows" {
		in, err := os.OpenFile("CONIN$", os.O_RDWR, 0)
		if err != nil {
			return err
		}
		defer in.Close()
		out, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0)
		if err != nil {
			return err
		}
		defer out.Close()
		return f(in, out)
	} else if tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil {
		defer tty.Close()
		return f(tty, tty)
	} else if term.IsTerminal(int(os.Stdin.Fd())) {
		return f(os.Stdin, os.Stdin)
	} else {
		return fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err)
	}
}

// ReadSecret reads a value from the terminal with no echo. The prompt is ephemeral.
func ReadSecret(prompt string) (s []byte, err error) {
	err = WithTerminal(func(in, out *os.File) error {
		printPrompt(out, prompt)
		defer clearLine(out)
		s, err = term.ReadPassword(int(in.Fd()))
		return err
	})
	return
}

// ReadPublic reads a value from the terminal. The prompt is ephemeral.
func ReadPublic(prompt string) (s []byte, err error) {
	err = WithTerminal(func(in, out *os.File) error {
		printPrompt(out, prompt)

View on GitHub (pinned to b74dce4cdb)

Solutions

  1. Provide the input non-interactively: pipe the secret or use age's key-file/identities options.
  2. Allocate a TTY: run the command in docker run -t, ssh -tt, or a CI step with a TTY action.
  3. Ensure /dev/tty exists and is accessible in the container (mount devtmpfs, fix permissions).
  4. Restructure the automation to pass the passphrase via an identities/key file instead of terminal input.
  5. Check the embedded %v error for why /dev/tty open failed (ENOENT vs EACCES) and fix that specific cause.
Defensive patterns

Strategy: fallback

Validate before calling

// Detect TTY availability before invoking interactive APIs
func hasTerminal() bool {
    if term.IsTerminal(int(os.Stdin.Fd())) { return true }
    f, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
    if err == nil { f.Close(); return true }
    return false
}

Try / catch

if !hasTerminal() {
    // use non-interactive path: key file, env var, or pre-provided secret
    return runNonInteractive()
}
err = term.ReadSecret(prompt) // may still fail; handle the /dev/tty error

Prevention

When it happens

Trigger: Calling ReadSecret/ReadPublic/ReadCharacter/printfToTerminal (which go through WithTerminal) in an environment where stdin is a pipe/file (not a TTY) and /dev/tty is absent or unopenable: cron/systemd services, CI without a TTY allocation, containers without a controlling terminal, or environments where /dev/tty permissions deny access.

Common situations: Running age decrypt in a script with piped stdin when the tool requires TTY input; Docker containers without -t; CI pipelines without a pseudo-TTY; ssh sessions with stdin redirected; minimal images lacking /dev/tty.

Related errors


AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31). Data as JSON: /api/errors/9d4aed36f2cd8f42. Report an issue: GitHub.