Skip to content

fix(desktop): app no longer opens a blank window when the Linux credential store is locked - #9246

Closed
selenodot wants to merge 3 commits into
pingdotgg:mainfrom
selenodot:claude/issue-9239-fix-wqardt
Closed

selenodot wants to merge 3 commits into
pingdotgg:mainfrom
selenodot:claude/issue-9239-fix-wqardt

Conversation

@selenodot

@selenodot selenodot commented Sep 2, 2026 •

Copy link
Copy Markdown

Fixes #9239.

What Changed

  • ElectronSafeStorage now uses Electron's promise-based encryptStringAsync / decryptStringAsync instead of the blocking isEncryptionAvailable / encryptString / decryptString, and gives up on a call the credential store leaves unanswered for 30 seconds.
  • Availability is answered by encrypting a throwaway probe string, because the dedicated availability call is synchronous in Electron 42 and promise-based in 43; encrypting resolves the same way on both.
  • Runtimes without the async calls (Electron below 42) keep the blocking ones, so nothing changes there.
  • On Linux the basic_text backend still counts as unavailable, matching what the blocking API reported: credentials go into a real keyring or nowhere, never under Chromium's hardcoded password.
  • The connection-catalog legacy migration now reads the legacy registry before it asks whether encryption is available, so a fresh install never waits on the keyring at all.
  • New ElectronSafeStorage tests cover the async path, the deadline, the basic_text refusal 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

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

🤖 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.

ElectronSafeStorage prefers Electron’s encryptStringAsync / 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 blocking isEncryptionAvailable. Linux basic_text is still treated as unavailable; older Electron without async APIs keeps the blocking fallback.

DesktopConnectionCatalogStore legacy 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 ElectronSafeStorage

  • When Electron's promise-based encryptString/decryptString APIs are available, ElectronSafeStorage.make uses them instead of the blocking synchronous calls, applying a 30-second deadline that converts unanswered requests into typed ElectronSafeStorageEncryptError failures or false availability.
  • Treats the Linux basic_text backend as unavailable and skips the encryption probe for it, so a locked credential store no longer blocks startup.
  • Reorders the DesktopConnectionCatalogStore.make migration to read the legacy registry before evaluating secure-storage availability, so empty registries never trigger an encryption check.
  • Runtimes without both async methods retain the existing synchronous encrypt/decrypt/availability path.
  • Risk: the new 30-second deadline in ElectronSafeStorage.ts means any async call that takes longer than 30s fails instead of hanging; callers in DesktopConnectionCatalogStore must handle the resulting error or false availability.

Macroscope summarized 36c1de7.

claude and others added 3 commits September 2, 2026 17:42
…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
@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Sep 2, 2026
Comment on lines +98 to +101
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.`,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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)),
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 36c1de7. Configure here.

@macroscopeapp

macroscopeapp Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: T3 Code opens a blank window when the Linux credential store is locked

2 participants