{"record":{"id":"78c5fb70d5b04337","repo":"can1357/oh-my-pi","slug":"failed-to-parse-ssh-config-file-filepath-err","errorCode":null,"errorMessage":"Failed to parse SSH config file ${filePath}: ${error.message}","messagePattern":"Failed to parse SSH config file (.+?): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/ssh/config-writer.ts","lineNumber":38,"sourceCode":"\thosts?: Record<string, SSHHostConfig>;\n}\n\n/**\n * Read an SSH config file.\n * Returns empty config if file doesn't exist.\n */\nexport async function readSSHConfigFile(filePath: string): Promise<SSHConfigFile> {\n\ttry {\n\t\tconst content = await fs.promises.readFile(filePath, \"utf-8\");\n\t\tconst parsed = JSON.parse(content) as SSHConfigFile;\n\t\treturn parsed;\n\t} catch (error) {\n\t\tif (isEnoent(error)) {\n\t\t\t// File doesn't exist, return empty config\n\t\t\treturn { hosts: {} };\n\t\t}\n\t\tif (error instanceof SyntaxError) {\n\t\t\tthrow new Error(`Failed to parse SSH config file ${filePath}: ${error.message}`);\n\t\t}\n\t\tthrow error;\n\t}\n}\n\n/**\n * Write an SSH config file atomically.\n * Creates parent directories if they don't exist.\n */\nexport async function writeSSHConfigFile(filePath: string, config: SSHConfigFile): Promise<void> {\n\t// Ensure parent directory exists\n\tconst dir = path.dirname(filePath);\n\tawait fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });\n\n\t// Write to temp file first (atomic write)\n\tconst tmpPath = `${filePath}.tmp`;\n\tconst content = JSON.stringify(config, null, 2);\n\tawait fs.promises.writeFile(tmpPath, content, { encoding: \"utf-8\", mode: 0o600 });","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/ssh/config-writer.ts#L20-L56","documentation":"readSSHConfigFile parses an SSH config file (user ~/.ssh/config or project config) and wraps parse failures. When the parser raises a SyntaxError (malformed SSH config syntax), it is rethrown with the file path and underlying message prefixed, so the developer knows which config file is broken. ENOENT is treated as an empty config; all other errors propagate unchanged.","triggerScenarios":"Calling readSSHConfigFile (directly or via addSSHHost/updateSSHHost/removeSSHHost, or reading user/project config) on a file with invalid SSH config syntax — unterminated patterns, garbage characters, invalid structures the parser rejects.","commonSituations":"Hand-edited ~/.ssh/config with typos, files generated by other tools in a format the parser doesn't accept, truncated files from interrupted writes, or non-UTF8/binary content in the config path.","solutions":["Open the file at the reported path and fix the syntax error indicated by error.message","Validate the file with `ssh -G <host>` or run ssh against it to locate offending lines","Restore the file from backup or version control","If the file is unrecoverable, move it aside and let the tooling write a fresh config"],"exampleFix":"# before (bad config)\nHost work\n  HostName\n# after\nHost work\n  HostName ssh.example.com","handlingStrategy":"try-catch","validationCode":"null","typeGuard":"null","tryCatchPattern":"try {\n  const cfg = await readSSHConfigFile(filePath);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith(\"Failed to parse SSH config file\")) {\n    // surface path, prompt user to fix or restore the config\n  } else throw err;\n}","preventionTips":["Validate ~/.ssh/config with `ssh -G` after hand edits","Keep configs in version control for easy restore","Avoid pointing the tool at generated/partial config files"],"tags":["ssh","config-parse","syntax-error"],"backgroundTag":"config-parse-error","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}