11 KiB
Claude Desktop sign-in — shipped fixes and planned work
Two related sign-in problems when Claude Desktop runs inside the LinuxServer Selkies streamed desktop.
Status:
- Shipped: Problem B (sign-in persistence) — the
autostartbootstrap landed in v1.4, and thegnome-keyringpackage itself was added in v1.17. That approach was later reverted: v1.24-ish removedgnome-keyringagain because it prompts for a keyring password on first boot, which blocked Claude Desktop from ever launching — but the launch flag was left forcing the now-daemonless libsecret backend, sosafeStoragesilently went unavailable again (recurring "sign in again", and the Claude app's dispatch tab showing the desktop as offline until a fresh sign-in was done from a computer). v1.35 switched to--password-store=basicplus a cont-init script that re-syncs the persistent openboxautostartfrom the image on every boot — but that flag alone does nothing, and the bug survived it untouched. Actually fixed in v1.37, which adds the application-side opt-in thebasicbackend requires; see "Why v1.35 did not work" below. - Planned only: Problem A (in-desktop browser for OAuth) is intentionally not implemented. The image ships no browser; complete the login with the user-side workaround below.
Problem A — Cannot complete login
Symptoms:
- "Continue with Google" does nothing.
- "Continue with email" sends a link that "must be opened from the same machine".
Root cause
Claude Desktop signs in through an external web page (Google OAuth or the email magic
link) and then hands the token back to the app via the claude:// URL scheme
(auto-registered on first launch). Both paths need:
- a web browser inside the streamed desktop to render the auth page, and
- a default-browser association so the app's
xdg-open <url>call goes somewhere.
The Selkies base image ships no browser and no default handler, so xdg-open has
nothing to dispatch to → Google "does nothing", and the email link's final claude://…
redirect only works on the machine running the app (the container), which currently can't
render/handle it.
Fix (image level)
In claude_desktop/Dockerfile, install a browser and register defaults so the whole flow
completes inside the session:
apt-get install -y --no-install-recommends chromium(Debian bookworm package name;firefox-esris an alternative).- Set the default browser and confirm the scheme handlers, e.g. as an s6 oneshot or in the
desktop
autostart(runs as the session user with$DISPLAY/dbus available):xdg-settings set default-web-browser chromium.desktop xdg-mime default claude-desktop.desktop x-scheme-handler/claude - Verifying expectations (for reference, not a test step):
x-scheme-handler/https→chromium.desktop,x-scheme-handler/claude→claude-desktop.desktop.
Then login is done entirely in the streamed desktop: Continue with Google opens
Chromium in-session and the claude:// redirect lands back in the app; or paste the email
magic link into the in-session Chromium (not a phone).
User-side workaround (no rebuild)
- Add-on Configuration →
additional_apps: chromium, restart (installed byrootfs/etc/cont-init.d/80-configuration.sh). - Add the two
xdg-settings/xdg-mimecommands to the custom script/addon_configs/db21ed7f_claude-desktop/claude_desktop.sh(the image ships no standalone terminal).
Problem B — "Your sign-in won't be saved on this device" / recurring re-auth / dispatch shows offline
Symptoms (all three are the same root cause):
-
Install and unlock a system keyring (such as GNOME Keyring), then restart the app.
- Periodic "For your security, sign in again to keep using Claude."
- The Claude app's dispatch tab shows this desktop as not online, even though the process is running — until a fresh sign-in is completed from a computer.
Root cause (history)
Claude Desktop (Electron) persists its auth token with safeStorage, which on Linux
encrypts via the Secret Service API (org.freedesktop.secrets) provided by
gnome-keyring/kwallet. --password-store=gnome-libsecret forces that backend.
This was fixed once (v1.17: gnome-keyring added to the Dockerfile) and then regressed:
gnome-keyring was later removed again because, on first boot, it prompts for a keyring
password and blocks Claude Desktop from launching at all — a worse failure than a lost
session. The launch flag was left forcing libsecret, so with no daemon running
safeStorage.isEncryptionAvailable() is false. Confirmed from a live install's
~/.config/Claude/logs/main.log:
[safeStorage] isEncryptionAvailable=false ... (backend=gnome_libsecret) — session will not persist; app secrets fall back to plaintext
Electron safeStorage encryption is not available on this system, cannot store allowlist cache
The un-persisted session goes stale and then fails the elevated-access OAuth check the cowork/dispatch bridge needs:
permission_error: "Session is not fresh enough to grant elevated access. Sign in again to continue." (session_stale_relogin)
[sessions-bridge] Cowork OAuth stale-session (session_stale_relogin); parking bridge until re-login
That "parked" bridge is exactly what the Claude app surfaces as this desktop being offline in the dispatch tab — a fresh sign-in (only completable from a computer, see Problem A) un-parks it, which is why "open from a computer first" appeared to fix it.
Re-adding gnome-keyring would just reintroduce the original launch-blocking prompt, so it's a dead end without also solving that — hence the shipped fix below avoids keyring entirely.
Why v1.35 did not work
v1.35 assumed --password-store=basic makes isEncryptionAvailable() "always true". It does
not. On a live install the flag was confirmed on the running process:
$ tr '\0' ' ' < /proc/2247/cmdline
/usr/lib/claude-desktop/claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=basic
and the app still logged, on every launch:
[warn] safeStorage not available, tokens will not persist
[warn] Encryption not available, returning empty env vars
[error] Electron safeStorage encryption is not available on this system, cannot store allowlist cache
Electron deliberately refuses its own basic_text backend unless the application opts in
by calling safeStorage.setUsePlainTextEncryption(true) before the ready event. The symbol
is present in the shipped Electron binary but absent from resources/app.asar — Claude Desktop
never calls it. So v1.35 replaced one unavailable backend with another.
Confirmed against a standalone Electron of the same generation, all with
--password-store=basic:
| case | isEncryptionAvailable() |
|---|---|
| no opt-in (= Claude Desktop as shipped) | false |
setUsePlainTextEncryption(true) |
true, encryptString works |
| fresh process, decrypting the earlier process's blob | true, plaintext recovered |
The third row is the one that matters: it is the restart survival this add-on needs.
Fix (shipped, v1.37)
claude_desktop/rootfs/defaults/autostartlaunches with--password-store=basicinstead ofgnome-libsecret— Electron's built-in fixed-key store: no daemon, no prompt. Secrets land under$HOME/.config/Claude, andHOME=/data/datais persistent add-on storage, so the session survives restarts.basictrades away OS-backed at-rest protection: unlikegnome-libsecret, its encryption key isn't gated by a keyring daemon, so any process able to read the persistent$HOME/.config/Claudeprofile can recover the saved credentials. 1b.claude_desktop/rootfs/etc/cont-init.d/86-claude_safestorage.sh+claude_desktop/rootfs/usr/local/bin/claude-safestorage-patch.js(new) supply the opt-in the flag depends on, by injectingsafeStorage.setUsePlainTextEncryption(true)into the app's main bundle insideapp.asar. The injection goes after the bundle's leading"use strict";— a directive prologue only counts as the first statement, so prepending ahead of it would drop the main process out of strict mode. The archive is rebuilt (asar headers store per-file offsets, so content cannot simply grow in place), preservingunpackedand symlink entries and recomputing the per-file SHA-256integrityrecord for the single changed file; the result is verified and only then renamed over the original. It re-runs every boot after81-claude_update.sh, because an apt upgrade ofclaude-desktopships a fresh unpatchedapp.asar; it is marker-guarded, so an unchanged app is a no-op, and a failure is logged rather than propagated (an unpatched app still runs, it just forgets the sign-in).- A passwordless keyring was considered instead (keeps libsecret encryption-at-rest without
a prompt) and rejected: the keyring DB would live in the same persistent volume as the
ciphertext it's "protecting," so it adds ~no real confidentiality in this single-user
self-hosted setup, for more moving parts than
basic. claude_desktop/rootfs/etc/cont-init.d/85-openbox_autostart.sh(new): the app is actually launched from the persistent$HOME/.config/openbox/autostart, which the base image'sinit-selkies-configoneshot only seeds from/defaults/autostartwhen the persistent copy is missing. Editing/defaults/autostartalone therefore never reaches an existing install's copy. This script re-syncs the persistent copy from the image on every boot (as root, before any s6-rc service starts), so this fix — and any futureautostartchange — reaches upgrades, not just fresh installs.gnome-keyringstays out of the Dockerfile.
One-time step after upgrading
The previously-stored session is already stale. Complete one sign-in from a computer (mobile still can't finish the OAuth flow itself, per Problem A) — the session then persists normally and dispatch stays online regardless of which device connects first afterward.
Files this plan touched
claude_desktop/rootfs/defaults/autostart— drop the keyring bootstrap; launch with--password-store=basic.claude_desktop/rootfs/etc/cont-init.d/85-openbox_autostart.sh— new in v1.35; syncs the persistent autostart from the image on every boot.claude_desktop/rootfs/etc/cont-init.d/86-claude_safestorage.shandclaude_desktop/rootfs/usr/local/bin/claude-safestorage-patch.js— new in v1.37; the app-sidesafeStorageopt-in that makes--password-store=basicactually take effect.claude_desktop/Dockerfile— corrected stale comment (gnome-keyring is not installed).claude_desktop/CHANGELOG.md/config.yaml— v1.35, then v1.37.
Problem A (in-desktop browser for OAuth) remains planned-only; not touched by this change.