Eugeny/tabby · error · Error

WinSCP not found

Error message

WinSCP not found

What it means

Thrown by `convertPrivateKeyFileToPuTTYFormat` when `SSHService.getWinSCPPath()` returns null - i.e. WinSCP is not registered on the system. The path is detected by reading the Windows registry key `HKCR\\WinSCP.Url\\DefaultIcon`; if the key is absent (WinSCP not installed) the conversion cannot proceed because it depends on WinSCP's key tooling.

Source

Thrown at tabby-ssh/src/services/ssh.service.ts:88

            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 {
                await this.platform.exec(winSCPcom, ['/keygen', tmpFile.path, '-o', tmpFile.path, '--old-passphrase', curPassphrase])
            } catch (error) {
                console.warn('Could not convert private key ', error)
                continue

View on GitHub (pinned to 14e2d60b9b)

Solutions

  1. Install WinSCP (the installer registers `WinSCP.Url`) on the Windows host and retry.
  2. If using a portable WinSCP, register the `WinSCP.Url\\DefaultIcon` registry key pointing at the portable executable.
  3. Guard callers: check `ssh.getWinSCPPath()` before offering the PuTTY-conversion flow and disable the UI if null.
  4. On non-Windows platforms, do not surface the WinSCP-dependent feature at all.

Example fix

// before
const path = this.getWinSCPPath()
if (!path) throw new Error('WinSCP not found')

// caller guard
const winScp = this.ssh.getWinSCPPath()
if (!winScp) {
    this.notifications.error('WinSCP is not installed; install it to use PuTTY key conversion')
    return
}
await this.ssh.convertPrivateKeyFileToPuTTYFormat(profile)
Defensive patterns

Strategy: validation

Validate before calling

function isWinScpAvailable (ssh: SSHService): boolean {
    return ssh.getWinSCPPath() !== null
}

if (!isWinScpAvailable(this.ssh)) {
    this.notifications.error('WinSCP is not installed; install it to use PuTTY key conversion')
    return
}
await this.ssh.convertPrivateKeyFileToPuTTYFormat(profile)

Type guard

function isWinScpPath (p: string | null): p is string { return typeof p === 'string' && p.length > 0 }

Try / catch

try {
    await this.ssh.convertPrivateKeyFileToPuTTYFormat(profile)
} catch (e) {
    if (e instanceof Error && e.message === 'WinSCP not found') {
        this.notifications.error('Install WinSCP to enable PuTTY key conversion')
        return
    }
    throw e
}

Prevention

When it happens

Trigger: Calling the conversion on a machine without WinSCP installed/registered, or where the registry entry was removed. The `wnr.getRegistryKey(...)` lookup yields no `['']` default value, so `getWinSCPPath()` returns null.

Common situations: WinSCP never installed; WinSCP uninstalled but registry entries left partially; portable WinSCP that does not register the `WinSCP.Url` protocol handler; running on macOS/Linux (no registry, wnr is a no-op).

Related errors


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