restic/restic · error

unable to read password: %w

Error message

unable to read password: %w

What it means

When no password was supplied via the CLI option, environment variables, or a password file, restic prompts on the terminal (gopts.Term.ReadPassword). This error wraps a failure of that terminal read itself - no usable TTY, EOF on input, or a canceled context - and is unrelated to whether the password is correct.

Source

Thrown at internal/global/global.go:240

// readPassword reads the password from a password file, the environment
// variable RESTIC_PASSWORD or prompts the user. If the context is canceled,
// the function leaks the password reading goroutine.
func readPassword(ctx context.Context, gopts Options, prompt string) (string, error) {
	if gopts.InsecureNoPassword {
		if gopts.Password != "" {
			return "", errors.Fatal("--insecure-no-password must not be specified together with providing a password via a cli option or environment variable")
		}
		return "", nil
	}

	if gopts.Password != "" {
		return gopts.Password, nil
	}

	password, err := gopts.Term.ReadPassword(ctx, prompt)
	if err != nil {
		return "", fmt.Errorf("unable to read password: %w", err)
	}

	if len(password) == 0 {
		return "", errors.Fatal("an empty password is not allowed by default. Pass the flag `--insecure-no-password` to restic to disable this check")
	}

	return password, nil
}

// ReadPasswordTwice calls ReadPassword two times and returns an error when the
// passwords don't match. If the context is canceled, the function leaks the
// password reading goroutine.
func ReadPasswordTwice(ctx context.Context, gopts Options, prompt1, prompt2 string) (string, error) {
	pw1, err := readPassword(ctx, gopts, prompt1)
	if err != nil {
		return "", err
	}
	if gopts.Term.InputIsTerminal() {

View on GitHub (pinned to a80be1478a)

Solutions

  1. Set RESTIC_PASSWORD_FILE (file mode 600) or pass --password-file for every non-interactive run
  2. Use RESTIC_PASSWORD_COMMAND with a secret manager when storing a file is not an option
  3. Provide a real terminal for interactive use (docker run -t, ssh -tt)
  4. Check that the job's context is not canceled while credentials are being read

Example fix

# before (cron job, no TTY)
restic -r s3:s3.amazonaws.com/bucket backup /data  # fails: unable to read password

# after
printf 's3cret' > /etc/restic/pass && chmod 600 /etc/restic/pass
RESTIC_PASSWORD_FILE=/etc/restic/pass restic -r s3:s3.amazonaws.com/bucket backup /data
Defensive patterns

Strategy: validation

Validate before calling

// decide before prompting whether a password source exists
if gopts.Password == "" &&
    os.Getenv("RESTIC_PASSWORD_FILE") == "" &&
    os.Getenv("RESTIC_PASSWORD_COMMAND") == "" &&
    !isTerminal(os.Stdin) {
    return errors.New("no TTY available; provide --password-file or RESTIC_PASSWORD_FILE")
}

Type guard

func isTerminal(f *os.File) bool {
    fi, err := f.Stat()
    return err == nil && fi.Mode()&os.ModeCharDevice != 0
}

Try / catch

password, err := resolvePassword(ctx, gopts, "enter password")
if err != nil {
    if errors.Is(err, context.Canceled) {
        return err
    }
    return fmt.Errorf("non-interactive run without a password source: %w", err)
}

Prevention

When it happens

Trigger: Running restic where stdin/stdout is not a terminal: cron jobs, systemd services, docker without -t, CI pipelines, ssh without TTY allocation; the surrounding context being canceled while the prompt is open; a closed or permission-denied /dev/tty; piped input ending before the password is entered.

Common situations: Automated nightly backups without RESTIC_PASSWORD_FILE configured; restic inside Docker or Kubernetes CronJobs; scripts that redirect stdin and still expect interactive prompting to work.

Related errors


AI-assisted analysis of restic/restic@a80be1478a (2026-08-15). Data as JSON: /api/errors/83e0c30aedb15c52. Report an issue: GitHub.