{"record":{"id":"1a603b394f4994ab","repo":"can1357/oh-my-pi","slug":"ssh-key-not-found-keypath","errorCode":null,"errorMessage":"SSH key not found: ${keyPath}","messagePattern":"SSH key not found: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/ssh/connection-manager.ts","lineNumber":250,"sourceCode":"\nasync function deleteHostInfoFromDisk(hostName: string): Promise<void> {\n\tconst path = getHostInfoPath(hostName);\n\ttry {\n\t\tawait fs.promises.unlink(path);\n\t} catch (err) {\n\t\tif (isEnoent(err)) return;\n\t\tlogger.warn(\"Failed to delete SSH host info\", { host: hostName, error: String(err) });\n\t}\n}\n\nasync function validateKeyPermissions(keyPath?: string, platform: SshPlatform = process.platform): Promise<void> {\n\tif (!keyPath) return;\n\tlet stats: fs.Stats;\n\ttry {\n\t\tstats = await fs.promises.stat(keyPath);\n\t} catch (err) {\n\t\tif (isEnoent(err)) {\n\t\t\tthrow new Error(`SSH key not found: ${keyPath}`);\n\t\t}\n\t\tthrow err;\n\t}\n\tif (!stats.isFile()) {\n\t\tthrow new Error(`SSH key is not a file: ${keyPath}`);\n\t}\n\tif (platform === \"win32\") return;\n\tconst mode = stats.mode & 0o777;\n\tif ((mode & 0o077) !== 0) {\n\t\tthrow new Error(`SSH key permissions must be 600 or stricter: ${keyPath}`);\n\t}\n}\n\nfunction buildCommonArgs(host: SSHConnectionTarget, options?: SSHArgsOptions): string[] {\n\tconst args = options?.allowStdin ? [] : [\"-n\"];\n\n\tif (supportsSshControlMaster(options?.platform)) {\n\t\targs.push(\"-o\", \"ControlMaster=auto\", \"-o\", `ControlPath=${CONTROL_PATH}`, \"-o\", \"ControlPersist=3600\");","sourceCodeStart":232,"sourceCodeEnd":268,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/ssh/connection-manager.ts#L232-L268","documentation":"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.","triggerScenarios":"Configuring an SSHConnectionTarget with a privateKeyPath/keyPath that points to a nonexistent file, or a relative path resolved from a different working directory.","commonSituations":"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.","solutions":["Verify the path: ls -l <keyPath>; correct the key path in your SSH config","Generate a key if missing: ssh-keygen -t ed25519 -f <keyPath>","Use an absolute path (or correct ~ expansion) instead of a relative one","If the key is on another machine, copy it: scp otherhost:.ssh/id_ed25519 <keyPath>"],"exampleFix":"// before\n{ name: \"prod\", host: \"10.0.0.5\", privateKeyPath: \"~/.ssh/id_rsa_prod\" } // file absent\n// after\n$ ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_prod\n{ name: \"prod\", host: \"10.0.0.5\", privateKeyPath: \"/home/me/.ssh/id_ed25519_prod\" }","handlingStrategy":"validation","validationCode":"import { isEnoent } from \"@oh-my-pi/pi-utils\";\nimport * as fs from \"node:fs/promises\";\nasync function assertKeyExists(keyPath?: string) {\n  if (!keyPath) return;\n  try { await fs.stat(keyPath); }\n  catch (err) { if (isEnoent(err)) throw new Error(`key missing: ${keyPath}`); throw err; }\n}\nawait assertKeyExists(target.privateKeyPath);","typeGuard":null,"tryCatchPattern":"try {\n  await connect(target);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith(\"SSH key not found:\")) {\n    const p = err.message.slice(\"SSH key not found:\".length).trim();\n    console.error(`Key file missing at ${p}; check your config`);\n  }\n  throw err;\n}","preventionTips":["Store absolute key paths in config","After generating a key, verify with ls -l before wiring it in","Don't commit keys to repos; document where keys live per environment","Use ~ consistently — confirm your shell/env expands it the same way the tool does"],"tags":["ssh","file-not-found","configuration","authentication"],"backgroundTag":"ssh-key-not-found","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}