{"record":{"id":"2cdb5a7e2eb25564","repo":"can1357/oh-my-pi","slug":"ssh-key-is-not-a-file-keypath","errorCode":null,"errorMessage":"SSH key is not a file: ${keyPath}","messagePattern":"SSH key is not a file: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/ssh/connection-manager.ts","lineNumber":255,"sourceCode":"\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\");\n\t}\n\n\targs.push(\"-o\", \"BatchMode=yes\", \"-o\", \"StrictHostKeyChecking=accept-new\");\n\n\tif (host.port) {","sourceCodeStart":237,"sourceCodeEnd":273,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/ssh/connection-manager.ts#L237-L273","documentation":"After confirming the key path exists, validateKeyPermissions checks that it is a regular file. This error is thrown when the configured key path exists but is not a regular file — e.g. a directory, symlink chain target issue, socket, or device node — since ssh cannot use it as an identity file.","triggerScenarios":"Setting privateKeyPath to a directory (like ~/.ssh itself), to a path that is a symlink to a directory, or to a fifo/device; also when a path expanded unexpectedly (empty keyPath segment resolving to cwd).","commonSituations":"Config points at ~/.ssh instead of ~/.ssh/id_ed25519; a mounted path where the key appears as a directory; a broken provisioning script that created the key path as a directory.","solutions":["Check what is at the path: ls -ld <keyPath>; point the config at the actual private key file","If a directory was created by mistake, remove it and generate/copy the key there as a file","Ensure you reference the private key, not the .pub file directory or an agent socket"],"exampleFix":"// before\nprivateKeyPath: \"/home/me/.ssh\"           // a directory\n// after\nprivateKeyPath: \"/home/me/.ssh/id_ed25519\" // the key file","handlingStrategy":"validation","validationCode":"import * as fs from \"node:fs/promises\";\nconst st = await fs.stat(target.privateKeyPath);\nif (!st.isFile()) throw new Error(\"privateKeyPath must be a regular key file, not a directory/special file\");","typeGuard":"function isKeyFile(stats: fs.Stats): boolean {\n  return stats.isFile();\n}","tryCatchPattern":"try {\n  await connect(target);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith(\"SSH key is not a file:\")) {\n    console.error(\"Point privateKeyPath at the key file itself (e.g. ~/.ssh/id_ed25519)\");\n  }\n  throw err;\n}","preventionTips":["Reference the private key file, never ~/.ssh or a directory","Check provisioning scripts don't mkdir the key path","Spot-check config paths with ls -ld when setup changes"],"tags":["ssh","configuration","filesystem","authentication"],"backgroundTag":"ssh-key-not-found","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}