getsops/sops · error

could not read passphrase: %v

Error message

could not read passphrase: %v

What it means

LazyScryptIdentity.Unwrap needs a passphrase to build a ScryptIdentity; it obtains one from the configured Passphrase callback. If that callback returns an error (user aborted the prompt, no TTY available, agent failure), it is wrapped as "could not read passphrase". The error reflects failure to obtain the secret, not that it was wrong.

Source

Thrown at age/encrypted_keys.go:99

// ScryptIdentity.
type LazyScryptIdentity struct {
	Passphrase func() (string, error)
}

var _ age.Identity = &LazyScryptIdentity{}

func (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {
	for _, s := range stanzas {
		if s.Type == "scrypt" && len(stanzas) != 1 {
			return nil, errors.New("an scrypt recipient must be the only one")
		}
	}
	if len(stanzas) != 1 || stanzas[0].Type != "scrypt" {
		return nil, age.ErrIncorrectIdentity
	}
	pass, err := i.Passphrase()
	if err != nil {
		return nil, fmt.Errorf("could not read passphrase: %v", err)
	}
	ii, err := age.NewScryptIdentity(pass)
	if err != nil {
		return nil, err
	}
	fileKey, err = ii.Unwrap(stanzas)
	return fileKey, err
}

func unwrapIdentities(location string, reader io.Reader, allowMultipleKeysPerLine bool) (ParsedIdentities, error) {
	b := bufio.NewReader(reader)
	p, _ := b.Peek(14) // length of "age-encryption" and "-----BEGIN AGE"
	peeked := string(p)

	switch {
	// An age encrypted file, plain or armored.
	case peeked == "age-encryption" || peeked == "-----BEGIN AGE":
		var r io.Reader = b

View on GitHub (pinned to 13442bb981)

Solutions

  1. Set SOPS_AGE_PASSPHRASE (or your configured passphrase env var) in non-interactive environments.
  2. Ensure a TTY is available or fix the SOPS_AGE_PASSPHRASE_CMD script so it exits 0 and prints the passphrase.
  3. Store the identity unencrypted in CI (with file permissions 600) to avoid interactive prompts entirely.

Example fix

// before (CI)
- run: sops -d secrets.yaml
// after
- run: |
    export SOPS_AGE_PASSPHRASE="$AGE_PASSPHRASE"
    sops -d secrets.yaml
Defensive patterns

Strategy: validation

Validate before calling

if os.Getenv("SOPS_AGE_PASSPHRASE") == "" && !term.IsTerminal(int(os.Stdin.Fd())) {
    return fmt.Errorf("no TTY and SOPS_AGE_PASSPHRASE not set: passphrase unavailable")
}

Try / catch

fileKey, err := identity.Unwrap(stanzas)
if err != nil && strings.Contains(err.Error(), "could not read passphrase") {
    // surface a clear CI hint: set SOPS_AGE_PASSPHRASE or provide a TTY
}

Prevention

When it happens

Trigger: Calling Unwrap on a scrypt stanza when i.Passphrase() errors — no interactive terminal to prompt in, CI run without SOPS_AGE_PASSPHRASE set, or the passphrase program/agent exits non-zero.

Common situations: Running sops in non-interactive CI where stdin is not a TTY and no SOPS_AGE_PASSPHRASE env var is set; a broken SOPS_AGE_PASSPHRASE_CMD script; piping commands so the prompt cannot read stdin.

Related errors


AI-assisted analysis of getsops/sops@13442bb981 (2026-09-01). Data as JSON: /api/errors/c776ee3173edfd74. Report an issue: GitHub.