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 = bView on GitHub (pinned to 13442bb981)
Solutions
- Set SOPS_AGE_PASSPHRASE (or your configured passphrase env var) in non-interactive environments.
- Ensure a TTY is available or fix the SOPS_AGE_PASSPHRASE_CMD script so it exits 0 and prints the passphrase.
- 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
- Always set SOPS_AGE_PASSPHRASE or a passphrase command in CI
- Test passphrase-based decryption in a non-interactive shell before deploying
- Prefer unencrypted key files with 600 permissions in automation
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
- incorrect passphrase
- failed to decrypt identity file: %v
- failed to read '%s': %w
- failed to copy age decrypted data into bytes.Buffer: %w
- failed to parse command %s: %w
AI-assisted analysis of getsops/sops@13442bb981 (2026-09-01).
Data as JSON: /api/errors/c776ee3173edfd74.
Report an issue: GitHub.