getsops/sops · error

failed to parse input as Bech32-encoded age public key: %w

Error message

failed to parse input as Bech32-encoded age public key: %w

What it means

parseRecipient matched the recipient as a hybrid age recipient (age1pq1 prefix) and called filippo.io/age's ParseHybridRecipient, which rejected the string — bad Bech32 checksum, wrong length, illegal characters, or truncated value. MasterKeyFromRecipient and Encrypt both funnel through parseRecipient, so encrypting/initializing a sops file with this recipient fails.

Source

Thrown at age/keysource.go:494

			errs = append(errs, err)
		} else {
			identities = append(identities, ids...)
			if len(ids) == 0 {
				unusedLocations = append(unusedLocations, location)
			}
		}
	}
	return identities, unusedLocations, errs
}

// parseRecipient attempts to parse a string containing an encoded age public
// key or a public ssh key.
func parseRecipient(recipient string) (age.Recipient, error) {
	switch {
	case strings.HasPrefix(recipient, "age1pq1"):
		parsedRecipient, err := age.ParseHybridRecipient(recipient)
		if err != nil {
			return nil, fmt.Errorf("failed to parse input as Bech32-encoded age public key: %w", err)
		}

		return parsedRecipient, nil
	case strings.HasPrefix(recipient, "age1") && strings.Count(recipient, "1") > 1:
		parsedRecipient, err := plugin.NewRecipient(recipient, pluginTerminalUI)
		if err != nil {
			return nil, fmt.Errorf("failed to parse input as age key from age plugin: %w", err)
		}
		return parsedRecipient, nil
	case strings.HasPrefix(recipient, "age1"):
		parsedRecipient, err := age.ParseX25519Recipient(recipient)
		if err != nil {
			return nil, fmt.Errorf("failed to parse input as Bech32-encoded age public key: %w", err)
		}

		return parsedRecipient, nil
	case strings.HasPrefix(recipient, "ssh-"):
		parsedRecipient, err := agessh.ParseRecipient(recipient)

View on GitHub (pinned to 13442bb981)

Solutions

  1. Re-copy the full recipient from `age-keygen -y key.txt` (or the plugin's public-key output) and verify it has the age1pq1 prefix with no whitespace.
  2. Validate the Bech32 string: correct character set (no 1, b, i, o except separators), correct length for the key type.
  3. If the key is actually a plain X25519 key, use its age1... form instead of forcing the hybrid prefix.
  4. Confirm sops and the age library versions support hybrid recipients; upgrade sops if the recipient was generated by a newer age tooling.
  5. Store recipients in .sops.yaml via quotes to prevent shell/YAML mangling.

Example fix

# before
# .sops.yaml
age: age1pq1t9h4x...   # truncated on copy
// failed to parse input as Bech32-encoded age public key: ...

# after
age-keygen -y key.txt > /tmp/pub
# .sops.yaml
age: age1pq1t9h4x...full-recipient...   # pasted completely, no whitespace
Defensive patterns

Strategy: validation

Validate before calling

// Go/CI pre-check for hybrid recipients
func validHybridRecipient(r string) bool {
    return strings.HasPrefix(r, "age1pq1") &&
        !strings.ContainsAny(r, " \t\n\r\"'")
}
// plus one-time: `sops --encryption-round-trip` or age round-trip with the key

Try / catch

rk, err := sopsage.MasterKeyFromRecipient(recipient)
if err != nil {
    if strings.Contains(err.Error(), "Bech32-encoded age public key") {
        return fmt.Errorf("recipient %q is not a valid bech32 key; re-copy from age-keygen -y", recipient)
    }
    return err
}

Prevention

When it happens

Trigger: parseRecipient receives a recipient starting with age1pq1; age.ParseHybridRecipient returns an error for a malformed hybrid recipient string.

Common situations: Copy-paste truncated the recipient string; manual typing introduced an 'i' vs 'l' / '0' vs 'o' Bech32 confusion; newline or whitespace included in the recipient config; mixing up a hybrid recipient with a plain X25519 recipient from an older age/sops version.

Understand the failure class

Related errors


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