# 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 `autostart` bootstrap landed in v1.4, and the `gnome-keyring` package itself was added in v1.17. That approach was later reverted: v1.24-ish removed `gnome-keyring` again 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, so `safeStorage` silently 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=basic` plus a cont-init script that re-syncs the persistent openbox `autostart` from 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 the `basic` backend 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: 1. a **web browser inside the streamed desktop** to render the auth page, and 2. a **default-browser association** so the app's `xdg-open ` 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-esr` is 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): ```sh 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 by `rootfs/etc/cont-init.d/80-configuration.sh`). - Add the two `xdg-settings`/`xdg-mime` commands 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) 1. `claude_desktop/rootfs/defaults/autostart` launches with `--password-store=basic` instead of `gnome-libsecret` — Electron's built-in fixed-key store: no daemon, no prompt. Secrets land under `$HOME/.config/Claude`, and `HOME=/data/data` is persistent add-on storage, so the session survives restarts. `basic` trades away OS-backed at-rest protection: unlike `gnome-libsecret`, its encryption key isn't gated by a keyring daemon, so any process able to read the persistent `$HOME/.config/Claude` profile 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 injecting `safeStorage.setUsePlainTextEncryption(true)` into the app's main bundle inside `app.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), preserving `unpacked` and symlink entries and recomputing the per-file SHA-256 `integrity` record for the single changed file; the result is verified and only then renamed over the original. It re-runs every boot after `81-claude_update.sh`, because an apt upgrade of `claude-desktop` ships a fresh unpatched `app.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). 2. 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`. 3. `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's `init-selkies-config` oneshot only seeds from `/defaults/autostart` when the persistent copy is *missing*. Editing `/defaults/autostart` alone 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 future `autostart` change — reaches upgrades, not just fresh installs. 4. `gnome-keyring` stays 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.sh` and `claude_desktop/rootfs/usr/local/bin/claude-safestorage-patch.js` — new in v1.37; the app-side `safeStorage` opt-in that makes `--password-store=basic` actually 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.