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
- Provide the input non-interactively: pipe the secret or use age's key-file/identities options.
- Allocate a TTY: run the command in docker run -t, ssh -tt, or a CI step with a TTY action.
- Ensure /dev/tty exists and is accessible in the container (mount devtmpfs, fix permissions).
- Restructure the automation to pass the passphrase via an identities/key file instead of terminal input.
- 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
- Always provide a non-interactive input path for scripts, CI, and containers.
- Allocate a TTY (docker -t, ssh -tt) when interaction is required.
- Ensure /dev/tty exists and is accessible in minimal container images.
- Prefer identity/key files over typed passphrases in automation.
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
- ArmoredWriter already closed
- trailing data after armored file
- too much trailing whitespace
- too much leading whitespace
- empty line in armored data
AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31).
Data as JSON: /api/errors/9d4aed36f2cd8f42.
Report an issue: GitHub.