FiloSottile/age · error

malformed recipient %q: %v

Error message

malformed recipient %q: %v

What it means

tag.ParseRecipient decodes a Bech32 public key string with prefix "age1tag1" (or "age1tagpq1") using plugin.ParseRecipient. This error wraps a failure of that low-level parsing step — the string is not valid Bech32, has the wrong HRP/prefix, or fails plugin-level parsing — so no Recipient can be constructed from it.

Source

Thrown at tag/tag.go:46

)

// Recipient is a tagged P-256 or hybrid P-256 + ML-KEM-768 recipient.
//
// The latter recipient is safe against future cryptographically-relevant
// quantum computers, and can only be used along with other post-quantum
// recipients.
type Recipient struct {
	pk hpke.PublicKey
}

var _ age.Recipient = &Recipient{}

// ParseRecipient returns a new [Recipient] from a Bech32 public key
// encoding with the "age1tag1" or "age1tagpq1" prefix.
func ParseRecipient(s string) (*Recipient, error) {
	t, k, err := plugin.ParseRecipient(s)
	if err != nil {
		return nil, fmt.Errorf("malformed recipient %q: %v", s, err)
	}
	switch t {
	case "tag":
		r, err := NewClassicRecipient(k)
		if err != nil {
			return nil, fmt.Errorf("malformed recipient %q: %v", s, err)
		}
		return r, nil
	case "tagpq":
		r, err := NewHybridRecipient(k)
		if err != nil {
			return nil, fmt.Errorf("malformed recipient %q: %v", s, err)
		}
		return r, nil
	default:
		return nil, fmt.Errorf("malformed recipient %q: invalid type %q", s, t)
	}
}

View on GitHub (pinned to b74dce4cdb)

Solutions

  1. Check the wrapped %v for the exact Bech32/plugin parse failure (bad checksum, bad HRP, bad character).
  2. Verify the string starts with the correct prefix (age1tag1 for classic, age1tagpq1 for hybrid) and is unmodified.
  3. Regenerate or re-export the recipient string from the key holder rather than retyping it.
  4. Strip whitespace/newlines and keep the original casing before parsing.

Example fix

// before
r, err := tag.ParseRecipient(strings.TrimSpace(userInput) + "")
// after
s := strings.TrimSpace(userInput)
if !strings.HasPrefix(s, "age1tag1") && !strings.HasPrefix(s, "age1tagpq1") {
    return fmt.Errorf("not a tag recipient: %s", s)
}
r, err := tag.ParseRecipient(s)
Defensive patterns

Strategy: validation

Validate before calling

func looksLikeTagRecipient(s string) bool {
    s = strings.TrimSpace(s)
    return strings.HasPrefix(s, "age1tag1") || strings.HasPrefix(s, "age1tagpq1")
}
if !looksLikeTagRecipient(userInput) {
    return errors.New("not a tag recipient")
}
r, err := tag.ParseRecipient(userInput)

Prevention

When it happens

Trigger: Calling tag.ParseRecipient(s) where s fails plugin.ParseRecipient: invalid Bech32 checksum, wrong HRP (not the expected age/tag prefix), mixed case, or invalid characters.

Common situations: Users pasting an X25519 age1... key instead of a tag recipient; truncated or hand-edited recipient strings; copy/paste introducing whitespace or case changes; confusing age1tag1 with age1tagpq1 keys.

Understand the failure class

Related errors


AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31). Data as JSON: /api/errors/c16f94854a7b3ab7. Report an issue: GitHub.