[BUG] Desktop Linux on ChromeOS/Crostini: silent fallback to unencrypted storage — "sign-in won't be saved" despite working gnome-keyring

Status Open
Maintainer reply None cached
Activity 5 comments · opened Jul 15, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

NB: I have been in the backseat for this thing. The issue is definitely real, and the solution has definitely been working as I've restarted Claude Desktop quite a few times. But it's been Claude Code fixing itself this entire time, including this bug report. I'm just the supervisor, I guess! I can't vouch for all of it... or understand it.

On ChromeOS Crostini, AKA Linux on ChromeOS, (official claude-desktop .deb, Debian 13 container), every launch shows:

⚠️ Your sign-in won't be saved on this device. Install and unlock a system keyring (such as GNOME Keyring), then restart the app.

The warning is misleading: GNOME Keyring is installed, running, and unlocked. The app's session token is stored unencrypted and sign-in does not persist.

Root cause: Chromium's os_crypt backend auto-detection picks the portal backend (org.freedesktop.impl.portal.Secret). On Crostini this D-Bus name is activatable but has no working implementation — neither xdg-desktop-portal-gtk nor ChromeOS's desktop.cros portal backend provides the Secret interface. Init fails (Local State records "os_crypt":{"portal":{"prev_init_success":false}}) and the app silently falls back to unencrypted storage instead of trying the functional gnome-libsecret backend. A contributing factor is that Crostini sets XDG_CURRENT_DESKTOP=X-Generic, which defeats desktop-environment-based detection.

Launching with claude-desktop --password-store=gnome-libsecret fully resolves it: a Claude Safe Storage item (schema chrome_libsecret_os_crypt_password_v2) appears in the default keyring collection, the warning stops, and sign-in survives app/VM restarts.

Given ChromeOS's Linux environment is a large population of Debian-based desktop Linux users, this likely affects most Chromebook installs of the official .deb. Independent report of the same issue + same fix: https://note.com/sho_ai_magic3975/n/n410966a4f543

What Should Happen?

When the portal Secret backend fails to initialize, the app should fall back to probing org.freedesktop.secrets directly (gnome-libsecret) before resorting to unencrypted storage — or the launcher/.desktop file should detect Crostini (e.g. SOMMELIER_VERSION env var, or XDG_CURRENT_DESKTOP=X-Generic combined with a functioning org.freedesktop.secrets) and pass --password-store=gnome-libsecret.

At minimum, the warning should be actionable: tell the user which backend was tried and that --password-store=gnome-libsecret exists. The current message sends users hunting for a keyring they already have installed and unlocked.

Error Messages/Logs

UI warning: "Your sign-in won't be saved on this device. Install and unlock a system keyring (such as GNOME Keyring), then restart the app."

~/.config/Claude/"Local State" after failed init:
{"os_crypt":{"portal":{"prev_desktop":"X-Generic","prev_init_success":false}},...}

Keyring is demonstrably healthy (daemon running, default collection unlocked):
$ gdbus call --session --dest org.freedesktop.secrets --object-path /org/freedesktop/secrets/collection/Default_5fKeyring --method org.freedesktop.DBus.Properties.Get string:org.freedesktop.Secret.Collection string:Locked
(<false>,)

After launching with --password-store=gnome-libsecret, the safe-storage key is created and persists:
Item label: 'Claude Safe Storage'
Attributes: {'application': 'Claude', 'xdg:schema': 'chrome_libsecret_os_crypt_password_v2'}

Steps to Reproduce

  1. On a Chromebook with Linux (Crostini) enabled — Debian 12/13 container — install the official package: sudo apt install claude-desktop (from the downloads.claude.ai apt repo)
  2. Ensure gnome-keyring is installed and unlocked (sudo apt install gnome-keyring; default collection unlocked — verifiable via the gdbus call in the logs section)
  3. Launch Claude Desktop (launcher icon or claude-desktop in the terminal) and sign in
  4. Observe the "Your sign-in won't be saved on this device" warning banner despite the working keyring; note "prev_init_success":false under os_crypt.portal in ~/.config/Claude/"Local State", and no Claude Safe Storage item in the keyring
  5. Quit, relaunch as claude-desktop --password-store=gnome-libsecret
  6. Warning is gone; Claude Safe Storage item now exists in the keyring; sign-in persists across app and VM restarts

Claude Model

Sonnet (default)

Is this a regression?

No, this never worked

Last Working Version

_No response_

Claude Code Version

Claude Desktop 1.21459.0 (Electron 42.5.1), official .deb

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

Other

Additional Information

Workaround for affected users until patched:

Override the desktop entry so launcher launches carry the flag:

cp /usr/share/applications/com.anthropic.Claude.desktop ~/.local/share/applications/
sed -i 's|^Exec=claude-desktop|Exec=env XDG_CURRENT_DESKTOP=GNOME claude-desktop --password-store=gnome-libsecret|' ~/.local/share/applications/com.anthropic.Claude.desktop
update-desktop-database ~/.local/share/applications

And for terminal launches, a wrapper in ~/.bashrc:

claude-desktop() {
    command claude-desktop --password-store=gnome-libsecret "$@"
}

Environment: ChromeOS Crostini (termina VM / penguin container), Debian 13 trixie, Wayland via sommelier, XDG_CURRENT_DESKTOP=X-Generic, gnome-keyring 0.21.7 (libsecret 0.21.7).

(Related but separate ChromeOS-side issue, being reported to Google: on a cold Crostini VM start, the ChromeOS launcher's app-launch request is dropped by vm_cicerone — "Garcon channel not connected after 5 seconds" — and never retried, so the app doesn't open even once storage is fixed. Not an Anthropic bug; noted so Chromebook users finding this issue have the complete picture.)

View original on GitHub ↗

3 Comments

srgvg · 2 months ago

Same bug outside Crostini — reproduced on Sway/wlroots, which suggests the trigger is broader than ChromeOS.

Environment: claude-desktop 1.24012.9 (official apt), Debian 13 trixie amd64, kernel 7.1.3, sway 1.12 / wlroots 0.20, XDG_CURRENT_DESKTOP=Sway, xdg-desktop-portal 1.20.3, xdp-gtk 1.15.3, xdp-gnome 48.0, gnome-keyring 48.0.

Identical symptom and identical Local State evidence:

{"os_crypt":{"portal":{"prev_desktop":"Sway","prev_init_success":false}}}

gnome-keyring running, org.freedesktop.secrets on the bus, default collection unlocked.

The mechanism here is slightly different from X-Generic defeating DE detection. On a non-GNOME Wayland compositor, xdg-desktop-portal binds no Secret backend at all: only gnome-keyring.portal and kwallet.portal implement org.freedesktop.impl.portal.Secret, and gnome-keyring.portal declares UseIn=gnome. With default=gtk;wlr in portals.conf and no explicit Secret route, the interface has no implementation to route to.

A second workaround, without --password-store: add an explicit route to ~/.config/xdg-desktop-portal/portals.conf

org.freedesktop.impl.portal.Secret=gnome-keyring

then systemctl --user restart xdg-desktop-portal. prev_init_success flips to true and sign-in survives restarts.

I A/B tested this to rule out the portal restart being the actual fix — both arms included an identical restart:

| portals.conf | busctl --user introspect org.freedesktop.portal.Desktop /org/freedesktop/portal/desktop \| grep -c org.freedesktop.portal.Secret |
|---|---|
| no Secret route | 0 |
| org.freedesktop.impl.portal.Secret=gnome-keyring | 1 |

So the explicit route is what binds the backend, and notably it does take effect despite the backend's own UseIn=gnome.

This supports the report's core request from another angle: the portal path can work here, so the silent fallback to unencrypted storage is the defect rather than the portal being genuinely unavailable.

Diagnostic note for anyone reproducing: don't introspect org.freedesktop.impl.portal.Secret to check whether a backend is bound. gnome-keyring.portal sets DBusName=org.freedesktop.secrets, so that bus name never exists and the call simply times out — which reads exactly like "still broken". Probe the frontend instead:

busctl --user introspect org.freedesktop.portal.Desktop /org/freedesktop/portal/desktop | grep -i secret

Also worth adding to repro instructions: the app must be fully killed between attempts, not window-closed (see #78798 §3) — otherwise the surviving process absorbs the relaunch and neither new flags nor new portal config take effect. This produced several misleading test runs before I noticed.

ocewers · 2 months ago

Third environment (Hyprland), but the useful part is below: I went through the bundled app.asar and found the exact branch where this decision is made.

Environment: claude-desktop 1.24012.9 (Electron 42.7.0, Chrome 148.0.7778.280), Arch/Omarchy, Hyprland 0.56.0, XDG_CURRENT_DESKTOP=Hyprland, gnome-keyring 50.0, org.freedesktop.secrets on the bus, default collection unlocked. Installed from the AUR package, which extracts the official .deb payload verbatim and only adds Debian→Arch path symlinks for the Cowork VM stack (plus dropping the Debian-only lintian files) — so this is upstream's build, not a repackaging artifact.

Same fingerprint as the two reports above:

{"os_crypt":{"portal":{"prev_desktop":"Hyprland","prev_init_success":false}}}
[warn] [safeStorage] isEncryptionAvailable=false on linux at startup (backend=basic_text) — session will not persist

--password-store=gnome-libsecret resolves it completely: Claude Safe Storage item created, warning gone, sign-in persists.

Why it happens, in the app's own code

The app doesn't rely solely on Chromium's detection — it has its own:

const desktop = () => {
  const d = (process.env.XDG_CURRENT_DESKTOP ?? "").toUpperCase().split(":");
  return d.includes("KDE")   ? "kde"
       : d.includes("GNOME") ? "gnome"
       : (!process.env.XDG_CURRENT_DESKTOP && process.env.KDE_FULL_SESSION) ? "kde"
       : "other";
};

password-store occurs four times in the bundle — three in code, one inside a log string:

  1. A kwallet pre-flight, guarded by desktop() !== "kde" → early return
  2. desktop() === "kde" && !commandLine.hasSwitch("password-store") → probe org.kde.kwalletd{5,6} over D-Bus
  3. appendSwitch("password-store", "basic") — when kwalletd is reachable but has no wallet

Which means:

| desktop() | Behaviour |
|---|---|
| kde | Probed over D-Bus and handled |
| gnome | Detected — but the only use I can find for the value is picking a tray icon, never password-store. Works anyway, because Chromium recognises GNOME. |
| other | No branch at all. Falls through to Chromium's failed-portal autodetect. |

The app never calls appendSwitch("password-store", "gnome-libsecret") anywhere — the only value it ever sets is "basic". Crostini (X-Generic), Sway and Hyprland all land in other and inherit the fallback.

The fix mirrors code that already exists

The KDE path already probes D-Bus for the absence of a wallet in order to downgrade safely. What's missing is the same probe for the presence of a Secret Service, in order to upgrade:

if (desktop() === "other" && !app.commandLine.hasSwitch("password-store")) {
  // the same D-Bus helper already used for org.kde.kwalletd6
  // and org.freedesktop.portal.Desktop
  if (await dbusNameHasOwner("org.freedesktop.secrets"))
    app.commandLine.appendSwitch("password-store", "gnome-libsecret");
}

This is strictly safer than the KDE branch that already ships: it upgrades rather than downgrades, and only when the service actually answers on the bus.

Not a niche configuration

XDG_CURRENT_DESKTOP=Hyprland isn't something the user sets. It comes from DesktopNames=Hyprland in /usr/share/wayland-sessions/hyprland.desktop, shipped by the hyprland package itself — the same mechanism gives Sway its value. Every user of these compositors gets a string the app classifies as other, by default, with no way to influence it short of lying about their desktop (which breaks xdg-desktop-portal screen sharing).

hyprland is packaged in Debian (trixie-backports, forky, sid), so this reproduces on the official .deb on a supported distribution.

Secondary

The warning text should name the backend that was actually selected. "Install and unlock a system keyring (such as GNOME Keyring)" is actively misleading when a keyring is installed, running and unlocked — which is the case in all three reports on this issue. Something like "secure storage unavailable (backend: basic_text)" would at least be diagnosable.

joshuakraemer · 1 month ago

Still present in 1.40609.0.

My environment: Void Linux, labwc/wlroots, XDG_CURRENT_DESKTOP=labwc:wlroots → other in the desktop() branch @ocewers identified. claude-desktop installed from the official .deb package. gnome-keyring running, org.freedesktop.secrets on the bus, collection unlocked.

Same fingerprint:

{"os_crypt":{"portal":{"prev_desktop":"labwc:wlroots","prev_init_success":false}}}
key_storage_linux.cc:116] Selected backend for OSCrypt: BASIC_TEXT
key_storage_linux.cc:135] OSCrypt did not initialize a backend.

--password-store=gnome-libsecret fixes it.

The warning never reaches the user

Fresh, empty --user-data-dir. main.log has the detection:

[warn] [safeStorage] isEncryptionAvailable=false on linux at startup (backend=basic_text) — session will not persist; app secrets fall back to plaintext

but no insecure-storage notice scheduled line and no toast. 125 [info] lines in the same file, so the level isn't masking it; the profile was empty, so the persisted insecureStorageNoticeShown flag doesn't explain it either.

Even when it fires, a one-shot toast on a 5s timer — early-returning on a persisted flag, racing window readiness — is the wrong mechanism for a permanent condition that is re-detected at every startup.

Why it's hard to diagnose

The symptom isn't "sign-in won't be saved". The session cookie outlives the missing refresh token, so it presents as a forced magic-link login every 1–2 days, citing only security reasons — the session-expiry message. That reads as server-side policy, not local storage. Four days to find the Chromium line above, which needs --enable-logging=stderr --v=1 since child processes run with --disable-logging.

Suggestion

Besides the D-Bus probe @ocewers proposed: make the insecure-storage state persistent rather than a one-shot toast, and have the re-login prompt name the cause when isEncryptionAvailable() is false — that prompt is the one users actually see.

Showing cached comments. Read the full discussion on GitHub ↗