Eugeny/tabby · warning · Error

Vault unlock cancelled

Error message

Vault unlock cancelled

What it means

Thrown by `VaultService.getPassphrase` when the unlock modal is dismissed or returns a falsy result (user clicked cancel/closed the dialog/pressed ESC). Since there is no remembered passphrase, the only way to obtain one is the modal; cancelling it leaves no passphrase to proceed, so the operation aborts.

Source

Thrown at tabby-core/src/services/vault.service.ts:183

        }
        if (_rememberedPassphrase) {
            _rememberedPassphrase = passphrase
        }
        return wrapPromise(this.zone, encryptVault(vault, passphrase))
    }

    async save (vault: Vault, passphrase?: string): Promise<void> {
        await this.ready$.toPromise()
        this.store = await this.encrypt(vault, passphrase)
        this.contentChanged.next()
    }

    async getPassphrase (): Promise<string> {
        if (!_rememberedPassphrase) {
            const modal = this.ngbModal.open(UnlockVaultModalComponent)
            const result = await modal.result.catch(() => null)
            if (!result) {
                throw new Error('Vault unlock cancelled')
            }
            const { passphrase, rememberFor } = result
            setTimeout(() => {
                _rememberedPassphrase = null
                // avoid multiple consequent prompts
            }, Math.max(1000, rememberFor * 60000))
            _rememberedPassphrase = passphrase
        }

        return _rememberedPassphrase!
    }

    async getSecret (type: string, key: VaultSecretKey): Promise<VaultSecret|null> {
        await this.ready$.toPromise()
        const vault = await this.load()
        if (!vault) {
            return null
        }

View on GitHub (pinned to 14e2d60b9b)

Solutions

  1. Wrap the calling operation in try/catch and treat 'Vault unlock cancelled' as a user-initiated cancel (skip the action, do not retry automatically).
  2. If the operation is mandatory, re-prompt the user with clearer messaging rather than silently failing.
  3. Pre-unlock the vault at app start (via a login flow) so dependent operations already have `_rememberedPassphrase` set.
  4. For headless contexts, supply the passphrase programmatically to `vault.decrypt(store, passphrase)` / `encrypt(vault, passphrase)` to bypass the modal entirely.

Example fix

// before
const vault = await this.vault.load()  // may throw 'Vault unlock cancelled'

// after - treat cancel as a soft abort
try {
    const vault = await this.vault.load()
    return vault
} catch (e) {
    if (e instanceof Error && e.message === 'Vault unlock cancelled') {
        return null  // user chose not to unlock; degrade gracefully
    }
    throw e
}
Defensive patterns

Strategy: try-catch

Validate before calling

async function tryWithPassphrase (vault: VaultService, passphrase: string | null, fn: () => Promise<any>) {
    if (passphrase) {
        // pass explicitly to bypass the modal entirely
        return fn()
    }
    return fn()  // will trigger modal; caller must handle cancel
}

Try / catch

try {
    return await vault.load()
} catch (e) {
    if (e instanceof Error && e.message === 'Vault unlock cancelled') {
        return null  // graceful: user declined to unlock
    }
    throw e
}

Prevention

When it happens

Trigger: Any vault operation that needs the passphrase (decrypt, encrypt, getSecret, addSecret, retrieveFile via VaultFileProvider) when `_rememberedPassphrase` is null and the user dismisses the `UnlockVaultModalComponent`. The modal's `.result` promise rejects on dismiss, which is caught and normalized to null.

Common situations: User cancels the master-passphrase prompt because they forgot it, do not want to unlock right now, or hit ESC by accident; an automated/scripted flow has no UI to answer the modal.

Related errors


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