FiloSottile/age · error

couldn't start plugin: %w

Error message

couldn't start plugin: %w

What it means

This error is returned by the plugin client's Wrap/WrapWithLabels function when it cannot open a connection to the plugin's helper binary via openClientConnection for the 'recipient-v1' protocol. The underlying cause (exec failure, handshake failure, etc.) is wrapped with %w. It means the age plugin process could not be launched or the client could not speak the v1 recipient protocol with it.

Source

Thrown at plugin/client.go:80

	}
	return r.encoding
}

func (r *Recipient) Wrap(fileKey []byte) (stanzas []*age.Stanza, err error) {
	stanzas, _, err = r.WrapWithLabels(fileKey)
	return
}

func (r *Recipient) WrapWithLabels(fileKey []byte) (stanzas []*age.Stanza, labels []string, err error) {
	defer func() {
		if err != nil {
			err = fmt.Errorf("%s plugin: %w", r.name, err)
		}
	}()

	conn, err := openClientConnection(r.name, "recipient-v1")
	if err != nil {
		return nil, nil, fmt.Errorf("couldn't start plugin: %w", err)
	}
	defer conn.Close()

	// Phase 1: client sends recipient or identity and file key
	addType := "add-recipient"
	if r.identity {
		addType = "add-identity"
	}
	if err := writeStanza(conn, addType, r.encoding); err != nil {
		return nil, nil, err
	}
	if _, err := writeGrease(conn); err != nil {
		return nil, nil, err
	}
	if err := writeStanzaWithBody(conn, "wrap-file-key", fileKey); err != nil {
		return nil, nil, err
	}
	if err := writeStanza(conn, "extension-labels"); err != nil {

View on GitHub (pinned to b74dce4cdb)

Solutions

  1. Install the plugin binary matching the recipient prefix and ensure it is on PATH and executable.
  2. Test the plugin manually (run its CLI) to confirm it starts.
  3. Verify the recipient string prefix maps to the correct plugin name (age1yubikey1... -> age-plugin-yubikey).
  4. Inspect the wrapped %w error for the OS-level cause (exec: not found, permission denied, exec format error) and fix accordingly.
  5. If the plugin needs hardware/agent access, ensure the device or agent is available before encrypting.

Example fix

// before
recipient, _ := age.ParseRecipients("age1yubikey1q...") // plugin not installed
out, err := age.Wrap(...) // fails: couldn't start plugin: exec not found
// after
if _, err := exec.LookPath("age-plugin-yubikey"); err != nil {
    return fmt.Errorf("install age-plugin-yubikey first: %w", err)
}
out, err := age.Wrap(...)
Defensive patterns

Strategy: try-catch

Validate before calling

// Go: check plugin availability before encryption
if _, err := exec.LookPath("age-plugin-yubikey"); err != nil {
    return fmt.Errorf("plugin not installed or not on PATH: %w", err)
}

Try / catch

stanzas, labels, err := client.WrapWithLabels(fileKey)
if err != nil {
    var execErr *exec.Error
    if errors.As(err, &execErr) {
        return fmt.Errorf("plugin %q not installed: %w", execErr.Name, err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling WrapWithLabels/Wrap (via age.Encrypt with a plugin recipient like age1yubikey1...) when the plugin binary named by the recipient prefix is not installed, not on PATH, not executable, or exits before completing the 'recipient-v1' handshake.

Common situations: Plugin not installed (e.g. age-plugin-yubikey missing from PATH); wrong recipient prefix so the client execs a nonexistent binary; plugin binary lacks execute permission; plugin built for another platform; plugin crashes at startup because hardware/agent is missing (YubiKey unplugged, yubikey-agent not running).

Related errors


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