can1357/oh-my-pi · error · DestinationUnavailableError
the SFTP privateKey credential must be a filesystem path, no
Error message
the SFTP privateKey credential must be a filesystem path, not key contents
What it means
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.
Source
Thrown at packages/coding-agent/src/blob-broker/uploaders-self-hosted.ts:197
}
const host = requiredStringOption(config, "host");
const username = requireCredential(config, "username");
const directory = optionString(config, "path");
const publicBase = requiredStringOption(config, "publicBaseUrl");
httpBase(publicBase, "publicBaseUrl");
if (protocol === "sftp") {
const port = optionNumber(config, "port", 22) ?? 22;
const keyPath = credentialString(config, "privateKey");
const password = credentialString(config, "password");
if (password && !keyPath) {
throw new DestinationUnavailableError(
"ftp",
"SFTP password injection is unsupported by the shared SSH transport; configure a private-key path or SSH agent",
);
}
if (keyPath?.includes("-----BEGIN")) {
throw new DestinationUnavailableError(
"ftp",
"the SFTP privateKey credential must be a filesystem path, not key contents",
);
}
const connectionName = `blob-${username}-${host}-${port}`.replace(/[^A-Za-z0-9._-]/g, "-");
return {
destination: "ftp",
async upload(request) {
const filename = safeFileName(request);
await writeRemoteFile(
{ name: connectionName, host, username, port, ...(keyPath ? { keyPath } : {}) },
remotePath(directory, filename),
request.bytes,
{},
);
return publication("ftp", request, publicUrl(publicBase, directory, filename));
},
};View on GitHub (pinned to 9690622007)
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.
Example fix
// before
"credentials": { "privateKey": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXk..." }
// after
"credentials": { "privateKey": "/home/me/.ssh/id_ed25519" } Defensive patterns
Strategy: validation
Validate before calling
const keyPath = dest.credentials?.privateKey;
if (typeof keyPath === 'string' && keyPath.includes('-----BEGIN')) {
throw new Error('privateKey must be a file path; write the PEM key to disk and reference the path');
} Type guard
const isKeyPath = (v) => typeof v === 'string' && v.length > 0 && !v.includes('-----BEGIN') && !v.includes('\n'); Try / catch
try {
const uploader = createSelfHostedUploader('ftp', config);
} catch (err) {
if (err?.name === 'DestinationUnavailableError' && /must be a filesystem path/.test(err.message)) {
// write key contents to a 0600 file and update credentials.privateKey to the path
} else throw err;
} Prevention
- 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.
When it happens
Trigger: credentials.privateKey set to the full text of a PEM key (starts with "-----BEGIN ... PRIVATE KEY-----") while options.protocol = "sftp".
Common situations: 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.
Related errors
- Destination option protocol must be ftp, ftps, or sftp
- No model configured
- Azure OpenAI base URL is required. Set AZURE_OPENAI_BASE_URL
- Cannot register custom API "${api}": built-in API names are
- Unable to read OMP_AUTH_BROKER_ACCOUNT_POOL_FILE at ${filePa
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/30e7f3ff65cab347.
Report an issue: GitHub.