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

  1. Guard the caller: only invoke the conversion when `profile.options.privateKeys.length > 0`.
  2. If keys are expected, add one to the profile via the key picker before calling.
  3. Disable/hide the PuTTY-conversion UI affordance for profiles without keys.
  4. 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

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


AI-assisted analysis of Eugeny/tabby@14e2d60b9b (2026-08-12). Data as JSON: /api/errors/3d9f3e8189ebd0c6. Report an issue: GitHub.