Repository navigation
Conversation
…ntial store is locked Electron's safeStorage.isEncryptionAvailable/encryptString/decryptString run the OS credential-store lookup on the main thread. When GNOME Keyring, KWallet or KeePassXC is locked, that lookup never returns, so the main process stops pumping its message loop while the renderer is still loading: the window opens black, cannot be resized, and no unlock prompt ever appears. The desktop app hits this on the boot path, where the renderer asks for the connection catalog as soon as it starts. Route safe storage through Electron's promise-based calls, which resolve off the main thread, so a locked store leaves the UI responsive instead of frozen. A call that goes unanswered for 30 seconds is treated as "unavailable for now" rather than waiting forever; nothing is cached, so unlocking the store later starts working without a restart. Runtimes without the async calls (Electron below 42) keep the blocking ones. On Linux the `basic_text` backend still counts as unavailable, so credentials are never quietly written under a hardcoded password. Also stop the connection-catalog migration from consulting the credential store before it knows there is anything to migrate, so a fresh install never waits on the keyring at all.
fix(desktop): app no longer opens a blank window when the Linux credential store is locked
| const deadlineCause = (operation: string): Error => | ||
| new Error( | ||
| `Electron safe storage did not answer within ${Duration.toSeconds(SAFE_STORAGE_DEADLINE)} seconds while trying to ${operation}. The OS credential store is most likely locked.`, | ||
| ); |
There was a problem hiding this comment.
deadlineCause manufactures an Error purely to fill the required cause field of the encrypt/decrypt errors, and the only description of the timeout lives inside that synthetic cause — the error's own message still reports a generic "failed to encrypt a string." A deadline expiry has no underlying failure, so it should be modeled structurally instead (as elsewhere in the repo, e.g. NativeTelemetryHandshakeTimedOut({ timeoutMs })).
Consider a dedicated tagged error (or a structural field plus optional cause) that carries the timeout duration and the operation, and derive the message from those attributes; withDeadline's onDeadline can then return it and the service interface can declare it in the error channel.
Posted via Macroscope — Effect Service Conventions
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 36c1de7. Configure here.
| error, | ||
| }, | ||
| ).pipe(Effect.as(false)), | ||
| ), |
There was a problem hiding this comment.
Locked store can wipe catalog
High Severity
A timed-out availability probe is reported as encryption unavailable. Callers then treat an existing connection-catalog.json as missing, and the renderer caches that empty document for the session. After the keyring unlocks, the next catalog write can replace the on-disk encrypted catalog.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 36c1de7. Configure here.
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — The PR changes production credential-store and encrypted connection-catalog behavior through async operations, timeouts, probing, and fallback logic. An unresolved review finding identifies a potential path from timeout-driven unavailability to overwriting an existing catalog, so the sensitive runtime change requires human review. You can add or adjust custom eligibility rules. Learn more. |


Fixes #9239.
What Changed
ElectronSafeStoragenow uses Electron's promise-basedencryptStringAsync/decryptStringAsyncinstead of the blockingisEncryptionAvailable/encryptString/decryptString, and gives up on a call the credential store leaves unanswered for 30 seconds.basic_textbackend still counts as unavailable, matching what the blocking API reported: credentials go into a real keyring or nowhere, never under Chromium's hardcoded password.ElectronSafeStoragetests cover the async path, the deadline, thebasic_textrefusal and the blocking fallback; a new catalog-store test pins the "nothing to migrate, don't touch the keyring" path.Why
safeStorage.isEncryptionAvailable()and friends run the OS credential-store lookup on the main thread. With GNOME Keyring, KWallet or KeePassXC locked, that lookup never returns, so the main process stops pumping its message loop. The desktop app hits this on the boot path — the renderer asks for the connection catalog as soon as it starts — which is exactly the reported symptom: a black window that cannot be resized, with no unlock prompt and no error.The async calls resolve off the main thread, so a locked store now leaves the window responsive and the app usable; credential-backed features degrade instead of freezing everything, and the warning naming the selected backend lands in the desktop log. Nothing is cached, so a store the user unlocks later starts working on the next call rather than needing a restart. The 30 second deadline is deliberately generous: it is long enough for someone to answer an unlock prompt, since giving up early would hide the environments they just unlocked for.
Verified with
vp test run apps/desktop/src(58 files, 449 tests), plus targeted typecheck, lint and format for the touched files.UI Changes
None.
Checklist
🤖 Generated with Claude Code
https://claude.ai/code/session_01CSYNeCmmbXoG5YETHpo1Md
Generated by Claude Code
Note
Medium Risk
Changes credential-store and connection-catalog startup behavior on Linux, but adds timeouts and graceful degradation rather than indefinite blocking; encrypt/decrypt semantics are preserved with explicit fallbacks.
Overview
Fixes a blank, frozen desktop window when Linux keyring integration blocks on startup by moving credential-store work off the main thread and not probing encryption when there is nothing to migrate.
ElectronSafeStorageprefers Electron’sencryptStringAsync/decryptStringAsync(when present) with a 30s deadline, so a locked OS credential store no longer hangs the main process indefinitely. Encryption availability on that path is determined by a probe encrypt (works across Electron 42/43 API differences) instead of blockingisEncryptionAvailable. Linuxbasic_textis still treated as unavailable; older Electron without async APIs keeps the blocking fallback.DesktopConnectionCatalogStorelegacy migration now reads the saved-environment registry first and only then checks encryption, so fresh installs skip keyring access on boot.New tests cover async safe storage (deadlines,
basic_text, fallback) and the catalog path that must not touch the credential store when the registry is empty.Reviewed by Cursor Bugbot for commit 36c1de7. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Fix blank window on Linux by adding 30s timeout and async support to
ElectronSafeStorageencryptString/decryptStringAPIs are available,ElectronSafeStorage.makeuses them instead of the blocking synchronous calls, applying a 30-second deadline that converts unanswered requests into typedElectronSafeStorageEncryptErrorfailures orfalseavailability.basic_textbackend as unavailable and skips the encryption probe for it, so a locked credential store no longer blocks startup.DesktopConnectionCatalogStore.makemigration to read the legacy registry before evaluating secure-storage availability, so empty registries never trigger an encryption check.DesktopConnectionCatalogStoremust handle the resulting error orfalseavailability.Macroscope summarized 36c1de7.