Eugeny/tabby · warning · Error
No private keys in profile
Error message
No private keys in profile
What it means
Thrown by `SSHService.convertPrivateKeyFileToPuTTYFormat` when the given SSH profile's `options.privateKeys` array is empty. The function exists to convert an existing private key into PuTTY format for use with WinSCP; with no keys there is nothing to convert, so it fails fast rather than producing a meaningless empty file.
Source
Thrown at tabby-ssh/src/services/ssh.service.ts:84
}
let tmpFile: tmp.FileResult|null = null
if (profile.options.jumpHost) {
const jumpHostProfile = this.config.store.profiles.find(x => x.id === profile.options.jumpHost) ?? null
const xTunnelParams = await this.generateWinSCPXTunnelURI(jumpHostProfile)
uri += xTunnelParams.uri ?? ''
tmpFile = xTunnelParams.privateKeyFile ?? null
}
if (profile.options.host.includes(':')) {
uri += `@[${profile.options.host}]:${profile.options.port}${cwd ?? '/'}`
}else {
uri += `@${profile.options.host}:${profile.options.port}${cwd ?? '/'}`
}
return { uri, privateKeyFile: tmpFile?? null }
}
async convertPrivateKeyFileToPuTTYFormat (profile: SSHProfile): Promise<{ passphrase: string|null, privateKeyFile: tmp.FileResult|null }> {
if (profile.options.privateKeys.length === 0) {
throw new Error('No private keys in profile')
}
const path = this.getWinSCPPath()
if (!path) {
throw new Error('WinSCP not found')
}
let tmpPrivateKeyFile: tmp.FileResult|null = null
let passphrase: string|null = null
const tmpFile: tmp.FileResult = await tmp.file()
for (const pk of profile.options.privateKeys) {
let privateKeyContent: string|null = null
const buffer = await this.fileProviders.retrieveFile(pk)
privateKeyContent = buffer.toString()
await fs.writeFile(tmpFile.path, privateKeyContent)
const keyHash = crypto.createHash('sha512').update(privateKeyContent).digest('hex')
// need to pass an default passphrase, otherwise it might get stuck at the passphrase input
const curPassphrase = await this.passwordStorage.loadPrivateKeyPassword(keyHash) ?? 'tabby'
const winSCPcom = path.slice(0, -3) + 'com'
try {View on GitHub (pinned to 14e2d60b9b)
Solutions
- Guard the caller: only invoke the conversion when `profile.options.privateKeys.length > 0`.
- If keys are expected, add one to the profile via the key picker before calling.
- Disable/hide the PuTTY-conversion UI affordance for profiles without keys.
- Refactor the check upstream so the function is unreachable for keyless profiles.
Example fix
// before
async convertPrivateKeyFileToPuTTYFormat (profile: SSHProfile) {
if (profile.options.privateKeys.length === 0) throw new Error('No private keys in profile')
...
}
// caller guard
if (profile.options.privateKeys.length > 0) {
result = await this.ssh.convertPrivateKeyFileToPuTTYFormat(profile)
} Defensive patterns
Strategy: validation
Validate before calling
function profileHasKeys (profile: SSHProfile): boolean {
return Array.isArray(profile.options.privateKeys) && profile.options.privateKeys.length > 0
}
if (!profileHasKeys(profile)) {
throw new Error('No private keys in profile; add a key or use password auth')
} Type guard
function hasPrivateKey (profile: SSHProfile): profile is SSHProfile & { options: { privateKeys: string[] } } {
return Array.isArray(profile.options.privateKeys) && profile.options.privateKeys.length > 0
} Prevention
- Guard the caller with a length check before invoking the conversion.
- Disable PuTTY/WinSCP conversion UI for keyless profiles.
- Add a key via the picker before offering conversion.
- Keep the check upstream so the function is unreachable for keyless profiles.
When it happens
Trigger: Calling `convertPrivateKeyFileToPuTTYFormat(profile)` on a profile where `profile.options.privateKeys.length === 0`. Reachable when a profile uses password auth (no keys) but a code path assumes key auth, or when keys were removed from the profile.
Common situations: Profile configured for password-only auth but a PuTTY/WinSCP integration tries to convert keys; UI button enabled for a keyless profile; race where keys were cleared mid-session.
Related errors
- WinSCP not found
- Refusing access outside the target directory: ${relativePath
- Download cancelled
- ${profile.options.host}: jump host "${profile.options.jumpHo
- File handle is closed
AI-assisted analysis of Eugeny/tabby@14e2d60b9b (2026-08-12).
Data as JSON: /api/errors/3d9f3e8189ebd0c6.
Report an issue: GitHub.