can1357/oh-my-pi · error

SSH key not found: ${keyPath}

Error message

SSH key not found: ${keyPath}

What it means

validateKeyPermissions stats the configured private key path before building the ssh command. This error is thrown when the key file does not exist (ENOENT), because ssh would otherwise fail later with a vaguer auth error. It fires from buildRemoteCommand and the connection promise path whenever an explicit key path (identity file) is configured.

Source

Thrown at packages/coding-agent/src/ssh/connection-manager.ts:250

async function deleteHostInfoFromDisk(hostName: string): Promise<void> {
	const path = getHostInfoPath(hostName);
	try {
		await fs.promises.unlink(path);
	} catch (err) {
		if (isEnoent(err)) return;
		logger.warn("Failed to delete SSH host info", { host: hostName, error: String(err) });
	}
}

async function validateKeyPermissions(keyPath?: string, platform: SshPlatform = process.platform): Promise<void> {
	if (!keyPath) return;
	let stats: fs.Stats;
	try {
		stats = await fs.promises.stat(keyPath);
	} catch (err) {
		if (isEnoent(err)) {
			throw new Error(`SSH key not found: ${keyPath}`);
		}
		throw err;
	}
	if (!stats.isFile()) {
		throw new Error(`SSH key is not a file: ${keyPath}`);
	}
	if (platform === "win32") return;
	const mode = stats.mode & 0o777;
	if ((mode & 0o077) !== 0) {
		throw new Error(`SSH key permissions must be 600 or stricter: ${keyPath}`);
	}
}

function buildCommonArgs(host: SSHConnectionTarget, options?: SSHArgsOptions): string[] {
	const args = options?.allowStdin ? [] : ["-n"];

	if (supportsSshControlMaster(options?.platform)) {
		args.push("-o", "ControlMaster=auto", "-o", `ControlPath=${CONTROL_PATH}`, "-o", "ControlPersist=3600");

View on GitHub (pinned to 9690622007)

Solutions

  1. Verify the path: ls -l <keyPath>; correct the key path in your SSH config
  2. Generate a key if missing: ssh-keygen -t ed25519 -f <keyPath>
  3. Use an absolute path (or correct ~ expansion) instead of a relative one
  4. If the key is on another machine, copy it: scp otherhost:.ssh/id_ed25519 <keyPath>

Example fix

// before
{ name: "prod", host: "10.0.0.5", privateKeyPath: "~/.ssh/id_rsa_prod" } // file absent
// after
$ ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_prod
{ name: "prod", host: "10.0.0.5", privateKeyPath: "/home/me/.ssh/id_ed25519_prod" }
Defensive patterns

Strategy: validation

Validate before calling

import { isEnoent } from "@oh-my-pi/pi-utils";
import * as fs from "node:fs/promises";
async function assertKeyExists(keyPath?: string) {
  if (!keyPath) return;
  try { await fs.stat(keyPath); }
  catch (err) { if (isEnoent(err)) throw new Error(`key missing: ${keyPath}`); throw err; }
}
await assertKeyExists(target.privateKeyPath);

Try / catch

try {
  await connect(target);
} catch (err) {
  if (err instanceof Error && err.message.startsWith("SSH key not found:")) {
    const p = err.message.slice("SSH key not found:".length).trim();
    console.error(`Key file missing at ${p}; check your config`);
  }
  throw err;
}

Prevention

When it happens

Trigger: Configuring an SSHConnectionTarget with a privateKeyPath/keyPath that points to a nonexistent file, or a relative path resolved from a different working directory.

Common situations: Typo in the key path in config, key generated on another machine and never copied, key deleted or moved after generating an SSH key pair, or a fresh clone whose .env/config references ~/keys/id_rsa that does not exist.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/1a603b394f4994ab. Report an issue: GitHub.