{"record":{"id":"30e7f3ff65cab347","repo":"can1357/oh-my-pi","slug":"the-sftp-privatekey-credential-must-be-a-filesyste","errorCode":null,"errorMessage":"the SFTP privateKey credential must be a filesystem path, not key contents","messagePattern":"the SFTP privateKey credential must be a filesystem path, not key contents","errorType":"validation","errorClass":"DestinationUnavailableError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/blob-broker/uploaders-self-hosted.ts","lineNumber":197,"sourceCode":"\t}\n\tconst host = requiredStringOption(config, \"host\");\n\tconst username = requireCredential(config, \"username\");\n\tconst directory = optionString(config, \"path\");\n\tconst publicBase = requiredStringOption(config, \"publicBaseUrl\");\n\thttpBase(publicBase, \"publicBaseUrl\");\n\n\tif (protocol === \"sftp\") {\n\t\tconst port = optionNumber(config, \"port\", 22) ?? 22;\n\t\tconst keyPath = credentialString(config, \"privateKey\");\n\t\tconst password = credentialString(config, \"password\");\n\t\tif (password && !keyPath) {\n\t\t\tthrow new DestinationUnavailableError(\n\t\t\t\t\"ftp\",\n\t\t\t\t\"SFTP password injection is unsupported by the shared SSH transport; configure a private-key path or SSH agent\",\n\t\t\t);\n\t\t}\n\t\tif (keyPath?.includes(\"-----BEGIN\")) {\n\t\t\tthrow new DestinationUnavailableError(\n\t\t\t\t\"ftp\",\n\t\t\t\t\"the SFTP privateKey credential must be a filesystem path, not key contents\",\n\t\t\t);\n\t\t}\n\t\tconst connectionName = `blob-${username}-${host}-${port}`.replace(/[^A-Za-z0-9._-]/g, \"-\");\n\t\treturn {\n\t\t\tdestination: \"ftp\",\n\t\t\tasync upload(request) {\n\t\t\t\tconst filename = safeFileName(request);\n\t\t\t\tawait writeRemoteFile(\n\t\t\t\t\t{ name: connectionName, host, username, port, ...(keyPath ? { keyPath } : {}) },\n\t\t\t\t\tremotePath(directory, filename),\n\t\t\t\t\trequest.bytes,\n\t\t\t\t\t{},\n\t\t\t\t);\n\t\t\t\treturn publication(\"ftp\", request, publicUrl(publicBase, directory, filename));\n\t\t\t},\n\t\t};","sourceCodeStart":179,"sourceCodeEnd":215,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/blob-broker/uploaders-self-hosted.ts#L179-L215","documentation":"A DestinationUnavailableError thrown when the sftp destination's credentials.privateKey contains PEM key material (\"-----BEGIN\") instead of a filesystem path. The shared SSH transport expects privateKey to name a key file on disk; embedding the key body would leak secrets into config and is rejected.","triggerScenarios":"credentials.privateKey set to the full text of a PEM key (starts with \"-----BEGIN ... PRIVATE KEY-----\") while options.protocol = \"sftp\".","commonSituations":"Users pasting a key from a cloud secret manager directly into config; CI secrets stores that hold key contents rather than files; converting a working password config and inlining the key body; confusion with tools (like some SFTP libs) that accept key contents.","solutions":["Write the PEM key to a file on disk and set credentials.privateKey to that file's path (e.g. /home/me/.ssh/id_ed25519).","Ensure the file has correct permissions (e.g. chmod 600) and no passphrase, or pre-load the passphrase into ssh-agent.","In CI, place the key as a secret FILE (checkout a temp path) rather than an inline string.","Never commit the key file; reference only its path in destination config."],"exampleFix":"// before\n\"credentials\": { \"privateKey\": \"-----BEGIN OPENSSH PRIVATE KEY-----\\nb3BlbnNzaC1rZXk...\" }\n// after\n\"credentials\": { \"privateKey\": \"/home/me/.ssh/id_ed25519\" }","handlingStrategy":"validation","validationCode":"const keyPath = dest.credentials?.privateKey;\nif (typeof keyPath === 'string' && keyPath.includes('-----BEGIN')) {\n  throw new Error('privateKey must be a file path; write the PEM key to disk and reference the path');\n}","typeGuard":"const isKeyPath = (v) => typeof v === 'string' && v.length > 0 && !v.includes('-----BEGIN') && !v.includes('\\n');","tryCatchPattern":"try {\n  const uploader = createSelfHostedUploader('ftp', config);\n} catch (err) {\n  if (err?.name === 'DestinationUnavailableError' && /must be a filesystem path/.test(err.message)) {\n    // write key contents to a 0600 file and update credentials.privateKey to the path\n  } else throw err;\n}","preventionTips":["Treat privateKey as a path field, never inline PEM — enforce it in config schema/docs.","In CI, write secrets as files (e.g. $RUNNER_TEMP/id_ed25519) and reference the path.","Check for '-----BEGIN' in any credential value during a config lint step and fail fast.","Set 0600 permissions on key files and keep them out of the repository."],"tags":["sftp","ssh-keys","configuration","secrets"],"backgroundTag":"invalid-credential-format","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}