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

  1. 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).
  2. Ensure the file has correct permissions (e.g. chmod 600) and no passphrase, or pre-load the passphrase into ssh-agent.
  3. In CI, place the key as a secret FILE (checkout a temp path) rather than an inline string.
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/30e7f3ff65cab347. Report an issue: GitHub.