jdx/mise · error

no public key for the SSH private key at {}; expected {}

Error message

no public key for the SSH private key at {}; expected {}

What it means

ssh_public_key_for_private derives an SSH recipient from a private key path by reading the sibling .pub file; it accepts the contents only if they start with "ssh-". If no sibling public key exists, or its contents are not a standard OpenSSH public key line, it bails naming both the private key and the expected .pub path.

Source

Thrown at src/agecrypt.rs:298

        .map_err(|e| eyre!("age recipient plugin is unavailable: {e}"))?;
        Ok(Some(Box::new(plugin)))
    } else {
        Ok(None)
    }
}

/// The public key beside an SSH private key: age cannot derive it, so the
/// `.pub` file must exist.
pub(crate) async fn ssh_public_key_for_private(path: &Path) -> Result<String> {
    let pub_path = path.with_extension("pub");
    if pub_path.exists() {
        let content = file::read_to_string(&pub_path)?;
        let trimmed = content.trim();
        if trimmed.starts_with("ssh-") {
            return Ok(trimmed.to_string());
        }
    }
    bail!(
        "no public key for the SSH private key at {}; expected {}",
        display_path(path),
        display_path(&pub_path)
    )
}

/// Every identity this machine has: `MISE_AGE_KEY`, the identity files
/// named by the settings and the default `age.txt`, and the SSH keys named
/// by the settings and the default `~/.ssh/id_ed25519` / `id_rsa`.
pub(crate) async fn load_all_identities() -> LoadedIdentities {
    load_identities(false).await
}

async fn load_identities(interactive: bool) -> LoadedIdentities {
    let identity_files = get_all_identity_files().await;
    let ssh_identity_files = get_all_ssh_identity_files();
    let mut loaded = LoadedIdentities::default();
    let mut plugin_sources = Vec::new();

View on GitHub (pinned to afd2eddd3a)

Solutions

  1. Generate the public key next to the private key: ssh-keygen -y -f <private> > <private>.pub.
  2. Ensure the .pub file contains a single OpenSSH line beginning with "ssh-" (e.g. ssh-ed25519 AAAA...).
  3. Point mise at a key pair whose .pub exists, or specify the public key explicitly as the recipient.

Example fix

// before
mise uses the key at ~/.ssh/id_rsa, but id_rsa.pub is missing
// error: no public key for the SSH private key at ~/.ssh/id_rsa; expected ~/.ssh/id_rsa.pub
// after
$ ssh-keygen -y -f ~/.ssh/id_rsa > ~/.ssh/id_rsa.pub
$ mise ... # now finds a valid ssh- prefixed public key
Defensive patterns

Strategy: validation

Validate before calling

use std::path::Path;
fn ssh_pair_ready(priv_key: &Path) -> bool {
    let pub_path = priv_key.with_file_name(format!("{}.pub", priv_key.file_name().unwrap().to_string_lossy()));
    std::fs::read_to_string(&pub_path)
        .map(|c| c.trim().starts_with("ssh-"))
        .unwrap_or(false)
}

Type guard

fn is_openssh_pubkey(s: &str) -> bool { s.trim().starts_with("ssh-") }

Try / catch

match ssh_public_key_for_private(&key_path) {
    Ok(pk) => use(pk),
    Err(e) if e.to_string().contains("no public key") => {
        let _ = std::process::Command::new("ssh-keygen")
            .args(["-y", "-f"]).arg(&key_path)
            .stdout(std::fs::File::create(key_path.with_extension("pub"))?)
            .status()?;
        ssh_public_key_for_private(&key_path)?
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: load_ssh_recipient_from_path or default_recipient_strings is given a private key whose corresponding <path>.pub is missing, unreadable, empty, or in a non-OpenSSH format (not starting with "ssh-", e.g. an RFC4716 or PEM export).

Common situations: User passes ~/.ssh/id_rsa but never generated id_rsa.pub; the .pub file was deleted or renamed; keys converted from PuTTY/PEM with a non-standard public-key blob.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/1b68a360e479c375. Report an issue: GitHub.