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
- Generate the public key next to the private key: ssh-keygen -y -f <private> > <private>.pub.
- Ensure the .pub file contains a single OpenSSH line beginning with "ssh-" (e.g. ssh-ed25519 AAAA...).
- 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
- Always generate key pairs with ssh-keygen so the .pub sibling exists.
- Ensure .pub files are OpenSSH-format single lines starting with "ssh-".
- Convert non-OpenSSH keys (PEM/PuTTY) before using them as age/SSH recipients.
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
- brew-cask: installer executable '{}' was not found
- brew-cask: font artifact '{}' was not staged
- brew-cask: binary artifact '{}' was not staged
- brew-cask: command wrapper '{}' was not staged
- remote command on '{}' failed with {status}
AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09).
Data as JSON: /api/errors/1b68a360e479c375.
Report an issue: GitHub.