mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-09-17 15:19:08 +02:00
* Add one-shot app_config linter migration workflow * Add app_config linter compatibility action * Add add-on map compatibility normalizer * Test add-on map compatibility normalizer * Run app_config compatibility preparation on pull request * Validate app_config short and long map forms separately * Export validated workflow updates * Use app_config-compatible linter for PR checks * Use app_config-compatible linter for builds * Remove temporary linter preparation workflow * Add one-shot app_config manifest migration * Migrate legacy addon_config maps to app_config * Preserve upstream legacy aliases without deprecated literals * Test legacy alias compatibility without deprecated literals * Validate completed app_config migration * Count only add-on manifests during migration validation * Remove temporary migration validation workflow * Document legacy alias construction * Remove obsolete local linter compatibility script * Remove obsolete local linter compatibility tests * fix(pr-2937-review): address reviewer findings on app_config migration - Remove the orphaned .github/actions/addon-linter composite action: it references .github/scripts/prepare_addon_lint_config.py, which was never added (the normalization approach was superseded by pinning frenck/action-addon-linter directly in #2936). Confirmed unused - no workflow invokes it. - cleanuparr: HA_DATA_DIR pointed at /app_configs/cleanuparr, a path not mounted inside the container. cleanuparr's map is app_config:rw, which Supervisor mounts at /config, so data was living on the ephemeral container filesystem. Point it at /config instead (pre-existing bug, not introduced by this PR - just carried the wrong path forward). - joplin/README.md: joplin's config.yaml still declares config:rw, not app_config (it was never part of the 86-manifest migration). Revert the doc text that incorrectly started calling it the app_config mapping. - qbittorrent/00-folders.sh: fix a pre-existing copy/paste bug in the migration marker/log for the main-folder migration block - it referenced "openvpn" and a "qBitorrent" typo instead of qBittorrent. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
227 lines
13 KiB
Markdown
227 lines
13 KiB
Markdown
# 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. v1.37 added the application-side opt-in the `basic` backend requires;
|
|
see "Why v1.35 did not work" below. That patcher then regressed silently when upstream's
|
|
bundle output changed shape — fixed again in 07308543.1; see "Why v1.37 stopped working"
|
|
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 <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-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
|
|
`/app_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.
|
|
|
|
### Why v1.37 stopped working
|
|
|
|
`claude-safestorage-patch.js` only knew how to inject its opt-in *after* a leading
|
|
`"use strict"` directive in the app's main bundle, and refused to patch (leaving the app
|
|
unpatched and the session un-persisted) if that directive wasn't there. Confirmed live on the
|
|
running add-on: Claude Desktop 1.30096.1's main bundle (`.vite/build/index.pre.js`) no longer
|
|
opens with a `"use strict"` directive — it now opens directly with a bare IIFE
|
|
(`(function(){try{var e=typeof window...`). Upstream's build output changed shape at some point
|
|
after v1.37 shipped, the patcher's one injection point stopped existing, and it had been
|
|
silently refusing to patch on every boot since — the app's `main.log` kept showing exactly the
|
|
same `Encryption not available, returning empty env vars` warning documented above, and the
|
|
session went back to not surviving restarts.
|
|
|
|
`applyPatch()` now falls back to inserting the opt-in as the bundle's first real statement when
|
|
no `"use strict"` directive is found, rather than refusing outright. It skips past any leading
|
|
BOM, hashbang, or banner comment first (`skipPrologue()`), so a directive hidden behind a
|
|
comment is still found and protected instead of being pushed out of the first-statement
|
|
position by a naive prepend — Vite/esbuild banners commonly put a license comment ahead of the
|
|
directive. A bundle with no directive at all has nothing to protect, so prepending the opt-in
|
|
there is unconditionally safe: the injected code is a complete `try{}catch(e){}` statement, and
|
|
a statement can never merge with what follows it via ASI the way a bare expression could.
|
|
|
|
Verified by copying the live production `app.asar` and running the patcher against it directly
|
|
(outside the container's boot sequence): the previously-refused bundle now patches
|
|
successfully, the marker lands at the front of the main entry, a second run correctly reports
|
|
"Already patched" (idempotent), and unit tests cover the bare-IIFE, comment-hidden-directive,
|
|
hashbang, and unterminated-comment cases.
|
|
|
|
### 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. This
|
|
applies again after the 07308543.1 fix above, since the affected sessions were never persisted
|
|
in the first place.
|
|
|
|
---
|
|
|
|
## 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-safestorage-patch.js` updated again in 07308543.1 to also patch bundles with no
|
|
leading `"use strict"` directive, and to look past leading comments/hashbang when deciding
|
|
whether one is present.
|
|
- `claude_desktop/Dockerfile` — corrected stale comment (gnome-keyring is not installed).
|
|
- `claude_desktop/CHANGELOG.md` / `config.yaml` — v1.35, then v1.37, then 07308543.1.
|
|
|
|
Problem A (in-desktop browser for OAuth) remains planned-only; not touched by this change.
|