fix(claude_desktop): persist sign-in via basic password store, fix GPU perms boot failure

Session logs on a live install showed safeStorage unavailable (no keyring daemon
behind the forced gnome-libsecret store) causing recurring "sign in again" prompts,
and the resulting stale session parking the cowork/dispatch bridge — surfacing as
the desktop showing offline in the Claude app when opened from mobile first.
Switch to --password-store=basic (no daemon, no first-boot prompt) and sync the
persistent openbox autostart from the image on every boot so the fix reaches
existing installs, not just fresh ones.

Also fix 21-gpu_permissions.sh exiting 2 at boot: getent's expected exit-2 for an
as-yet-unnamed DRI group, combined with bashio's pipefail + set -e, skipped the
script's own unnamed-group fallback before it could run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
alexbelgium
2026-07-23 08:19:53 +02:00
parent 61f72e155d
commit f0097ad467
7 changed files with 132 additions and 73 deletions

View File

@@ -1,3 +1,11 @@
## 1.35 (23-07-2026)
- Fix recurring "For your security, sign in again to keep using Claude." and the Claude app's dispatch tab showing this desktop as offline when opened from mobile first. Root cause (confirmed from `~/.config/Claude/logs/main.log` on a live install): the app launches with `--password-store=gnome-libsecret`, forcing Electron's libsecret/Secret-Service backend, but `gnome-keyring` was removed from the image in a previous commit because it prompted for a keyring password on first boot and blocked the app from launching. With the flag still forcing libsecret and no keyring daemon running, `safeStorage.isEncryptionAvailable()` is `false` — logs showed `session will not persist; app secrets fall back to plaintext` and `cannot store allowlist cache`. The un-persisted session then goes stale, failing the elevated-access OAuth check (`session_stale_relogin`) that the cowork/dispatch bridge needs, so the bridge is "parked until re-login" — which is what the Claude app surfaces as the desktop being offline, until a fresh sign-in (only completable from a computer, see `SIGN_IN.md` Problem A) un-parks it. `rootfs/defaults/autostart` now launches with `--password-store=basic` instead: Electron's built-in fixed-key store needs no daemon and never prompts, and persists under `$HOME/.config/Claude` (`/data/data`, persistent), so the session survives restarts and dispatch stays online regardless of which device connects first. A passwordless keyring was considered and rejected — it would live in the same persistent volume as the ciphertext, adding no real protection in this single-user self-hosted setup. `Dockerfile`'s stale comment (still describing gnome-keyring as installed) is corrected; the package stays removed.
- New `85-openbox_autostart.sh`: 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 — so on any existing install, editing `/defaults/autostart` alone would never reach the file actually being run. This new cont-init script re-syncs the persistent copy from the image on every boot (as root, before any s6-rc service starts, so it also overwrites a copy a prior boot may have left root-owned/read-only under `RESTART_APP`), so the `--password-store` change (and any future autostart change) reaches existing installs on upgrade, not just fresh ones.
- One-time step after upgrading: the previously-stored session is already stale, so complete one sign-in from a computer (mobile cannot complete the OAuth flow in the streamed desktop, see `SIGN_IN.md` Problem A). The session then persists normally.
- Fix `21-gpu_permissions.sh` failing at boot (`exiting 2`) while working fine when re-run manually. Root cause: the script runs under bashio, which sets `set -o pipefail`, together with the script's own `set -e`; `getent group "$gid" | awk ...` exits 2 whenever `$gid` has no named group yet, which is the normal case here because this script deliberately runs *before* the base image's `init-video`/`init-adduser` have named the passed-through `/dev/dri` gids. That exit 2 aborted the script immediately, before reaching its own `-z "$gname"` fallback (written specifically to create a `dri<gid>` group for exactly this unnamed case) — and, as a side effect, before the `chmod o+rw` fallback further down, so the boot-time GPU-permission hardening from 1.31 wasn't fully applied either. A manual re-run afterward always "worked" because by then `init-video` had already named the group. Fixed by not letting that expected exit propagate: `gname="$(getent group "$gid" | awk -F: '{print $1}')" || gname=""`.
## 1.34 (22-07-2026)
- Reduce idle and active remote-desktop overhead without changing Claude Desktop, Claude Code, GPU acceleration, Headroom, RTK, or TokenSave behavior. Selkies now defaults to 15 FPS instead of 30 FPS, uses browser-rendered cursors, and disables unused audio, microphone, gamepad, second-screen, and collaborative-sharing channels. All settings remain overridable through `env_vars` for installations that need those features.

View File

@@ -103,8 +103,9 @@ RUN if [ ! -f /bin/sh ] && [ -f /usr/bin/sh ]; then ln -s /usr/bin/sh /bin/sh; f
if [ ! -f /bin/bash ] && [ -f /usr/bin/bash ]; then ln -s /usr/bin/bash /bin/bash; fi
# Install Claude Desktop, Claude Code, Python tooling, and lightweight local validators.
# gnome-keyring provides the Secret Service backend Electron safeStorage needs to persist
# sign-in and dispatch grants.
# gnome-keyring is intentionally NOT installed: it prompts for a keyring password on first
# boot, which blocks Claude Desktop from launching. Sign-in persistence instead uses Electron's
# built-in --password-store=basic (rootfs/defaults/autostart) — no daemon, no prompt.
# The cowork virtualization stack (qemu-system-x86 + ovmf firmware) lets Claude Code launch
# its sandbox microVM; libseccomp2 and libcap-ng0 are the shared libraries the source-built
# virtiofsd daemon links against at runtime. Docker itself is NOT installed here: this base

View File

@@ -4,10 +4,16 @@ Two related sign-in problems when Claude Desktop runs inside the LinuxServer Sel
streamed desktop.
**Status:**
- **Shipped:** Problem B (keyring persistence) — the `autostart` bootstrap landed in v1.4, but
the `gnome-keyring` package itself was missing from the image until v1.17 (the bootstrap
silently no-oped and Electron logged "safeStorage encryption is not available"). Fixed in
v1.17: the Dockerfile now installs `gnome-keyring`.
- **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 — new in this round — the Claude app's dispatch tab
showing the desktop as offline until a fresh sign-in was done from a computer). Fixed in
v1.35: switched to `--password-store=basic` (Electron's built-in store, no keyring
involved at all) plus a cont-init script that re-syncs the persistent openbox `autostart`
from the image on every boot, so the fix reaches existing installs, not just fresh ones.
- **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.
@@ -59,64 +65,75 @@ magic link into the in-session Chromium (not a phone).
---
## Problem B — "Your sign-in won't be saved on this device"
## Problem B — "Your sign-in won't be saved on this device" / recurring re-auth / dispatch shows offline
Symptom:
> Install and unlock a system keyring (such as GNOME Keyring), then restart the app.
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
### 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. The Selkies base runs only a **system** D-Bus
(`svc-dbus` → `dbus-daemon --system`) and has **no keyring daemon**, so `safeStorage` is
unavailable and the token cannot be stored → the app warns and forgets the session on
restart.
gnome-keyring/kwallet. `--password-store=gnome-libsecret` **forces** that backend.
Note: the desktop itself already runs under a **session** bus —
`root/defaults/startwm.sh` launches `dbus-launch --exit-with-session openbox-session`, so
apps started from the openbox `autostart` inherit `DBUS_SESSION_BUS_ADDRESS`. The keyring
must be started **inside that session** so it registers the secrets service on the same bus
Claude Desktop uses. No extra `dbus-launch` is needed.
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.
### Fix (image level)
1. `claude_desktop/Dockerfile`: install
`gnome-keyring libsecret-1-0 dbus-x11` (dbus-x11 provides `dbus-launch`, already used by
startwm; libsecret-1-0 is the backend Electron dlopens).
2. `claude_desktop/rootfs/defaults/autostart`: start & unlock an empty-password keyring
before launching the app, and force the libsecret backend. Proposed content:
```sh
# Start + unlock a gnome-keyring secrets service so Claude Desktop can persist sign-in.
# Runs inside the openbox session's dbus (startwm.sh: dbus-launch --exit-with-session).
eval "$(printf '' | gnome-keyring-daemon --login)" 2>/dev/null || true
eval "$(gnome-keyring-daemon --start --components=secrets)" 2>/dev/null || true
dbus-update-activation-environment --all >/dev/null 2>&1 || true
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.
claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret
```
- `--login` reads the password from stdin (empty here), creating and unlocking the login
keyring on first run and starting the daemon in login mode; `--start --components=secrets`
then exposes the Secret Service and exports `GNOME_KEYRING_CONTROL`/`SSH_AUTH_SOCK`.
- `--password-store=gnome-libsecret` forces Electron to use the libsecret backend instead
of falling back to plaintext.
3. Persistence: the keyring DB lives in `$HOME/.local/share/keyrings/` and `HOME=/data/data`
(persistent add-on storage), so the empty-password login keyring survives restarts and is
re-unlocked automatically each boot by the same `autostart` line — the sign-in then sticks.
### Fix (shipped, v1.35)
1. `claude_desktop/rootfs/defaults/autostart` launches with
`--password-store=basic` instead of `gnome-libsecret`. `basic` is Electron's built-in
fixed-key store: `isEncryptionAvailable()` is always `true`, no daemon, no prompt. Secrets
land under `$HOME/.config/Claude`, and `HOME=/data/data` is persistent add-on storage, so
the session survives restarts.
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.
### User-side workaround (no rebuild)
- Add-on Configuration → `additional_apps: gnome-keyring, libsecret-1-0, dbus-x11`, restart.
- Add the keyring-start lines above to the custom script
`/addon_configs/db21ed7f_claude-desktop/claude_desktop.sh`, and relaunch Claude Desktop
with `--password-store=gnome-libsecret` (e.g. edit the in-session openbox autostart).
### 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 would touch (when implemented)
- `claude_desktop/Dockerfile` — install `chromium`, `gnome-keyring`, `libsecret-1-0`,
`dbus-x11`; set default browser + scheme handlers.
- `claude_desktop/rootfs/defaults/autostart` — keyring bootstrap + `--password-store` flag.
- `claude_desktop/CHANGELOG.md` / `config.yaml` version bump.
## 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; syncs the persistent
autostart from the image on every boot.
- `claude_desktop/Dockerfile` — corrected stale comment (gnome-keyring is not installed).
- `claude_desktop/CHANGELOG.md` / `config.yaml` — v1.35.
## Open questions before implementing
- Bundle **chromium** vs **firefox-esr** as the in-image browser?
- Empty-password keyring (fully unattended) vs. reuse the add-on `PASSWORD` option to lock
the keyring with the same password?
Problem A (in-desktop browser for OAuth) remains planned-only; not touched by this change.

View File

@@ -118,5 +118,5 @@ slug: claude_desktop
tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "1.34"
version: "1.35"
video: true

View File

@@ -1,18 +1,17 @@
#!/usr/bin/env sh
# Persist Claude Desktop sign-in across restarts.
# Claude Desktop (Electron) stores its auth token via safeStorage, which on Linux needs the
# Secret Service API (org.freedesktop.secrets). Provide it with gnome-keyring on the openbox
# session bus (startwm.sh runs `dbus-launch --exit-with-session`), unlocked with an empty
# password so it comes back automatically after a restart. The keyring DB lives under
# $HOME/.local/share/keyrings, and HOME=/data/data is persistent add-on storage, so the
# saved sign-in survives add-on restarts.
eval "$(gnome-keyring-daemon --start --components=secrets 2>/dev/null)" || true
printf '' | gnome-keyring-daemon --unlock 2>/dev/null || true
export GNOME_KEYRING_CONTROL SSH_AUTH_SOCK
dbus-update-activation-environment --all >/dev/null 2>&1 || true
# --password-store=gnome-libsecret forces Electron onto the libsecret backend instead of
# falling back to plaintext (and warning that the sign-in will not be saved). Headroom is
# intentionally not injected into the Desktop process: Claude Desktop force-overrides
# ANTHROPIC_BASE_URL (headroom #869), so Desktop uses the registered Headroom MCP tools.
exec claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret
# Claude Desktop (Electron) stores its auth token via safeStorage. On Linux the libsecret
# backend needs a running Secret Service (gnome-keyring), but gnome-keyring is intentionally
# not installed in this image: on first boot it prompts for a keyring password, which blocks
# Claude Desktop from ever launching. Without the daemon, forcing --password-store=gnome-libsecret
# leaves safeStorage.isEncryptionAvailable()=false, so the session can never be stored — that
# surfaced as recurring "sign in again" prompts and (because the stale session also fails the
# elevated-access OAuth check) the Claude app's dispatch tab showing this desktop as offline.
#
# --password-store=basic uses Electron's built-in fixed-key store instead: no daemon, no
# prompt, and isEncryptionAvailable() is always true. Secrets land under $HOME/.config/Claude,
# and HOME=/data/data is persistent add-on storage, so the saved sign-in survives restarts.
# Headroom is intentionally not injected into the Desktop process: Claude Desktop
# force-overrides ANTHROPIC_BASE_URL (headroom #869), so Desktop uses the registered Headroom
# MCP tools instead.
exec claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=basic

View File

@@ -34,7 +34,15 @@ for node in "${dri_nodes[@]}"; do
# the group when the GID is unnamed) but early enough that the s6-setuidgid at service
# start picks the membership up.
gid="$(stat -c '%g' "$node")"
gname="$(getent group "$gid" | awk -F: '{print $1}')"
# getent exits 2 when the gid has no named group, which is the common case here: this
# script deliberately runs before the base image's init-video/init-adduser have named the
# passed-through /dev/dri gids. Under bashio's `set -o pipefail` + this script's `set -e`,
# an unguarded pipeline would abort right here on that exit 2 — before the `-z "$gname"`
# fallback below (which exists precisely to handle an unnamed gid) ever runs. Confirmed by
# bashio manually re-running this script post-boot always "worked": by then the base
# image's own init-video oneshot had already named the group, so getent no longer hit its
# exit-2 path.
gname="$(getent group "$gid" | awk -F: '{print $1}')" || gname=""
if [ -z "$gname" ]; then
gname="dri${gid}"
groupadd -o -g "$gid" "$gname" 2> /dev/null || true

View File

@@ -0,0 +1,26 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# The openbox autostart that actually launches Claude Desktop lives on persistent storage at
# $HOME/.config/openbox/autostart. The base image's init-selkies-config s6-rc oneshot only
# seeds it from /defaults/autostart when the persistent copy is missing ("first run"), so an
# addon upgrade that changes /defaults/autostart (e.g. the --password-store flag) would
# otherwise never reach an existing install's persistent copy.
#
# cont-init.d runs to completion, as root, before any s6-rc service starts (same ordering
# 21-gpu_permissions.sh relies on) — i.e. before init-selkies-config's seed-if-absent check
# and before the RESTART_APP lockdown further down that same oneshot (which can leave the
# persistent file root-owned and chmod 550). Reconciling here, as root, overwrites that
# regardless, so every boot picks up the current /defaults/autostart content; ownership/mode
# is left in the normal abc-writable state that init-selkies-config itself uses when
# RESTART_APP is unset, and re-locked by that oneshot afterward if RESTART_APP is set.
if [ -f /defaults/autostart ]; then
mkdir -p "$HOME/.config/openbox"
cp -f /defaults/autostart "$HOME/.config/openbox/autostart"
chown abc:abc "$HOME/.config/openbox" "$HOME/.config/openbox/autostart"
chmod 644 "$HOME/.config/openbox/autostart"
bashio::log.info "Synced $HOME/.config/openbox/autostart from the current addon image"
else
bashio::log.warning "/defaults/autostart missing; leaving any existing openbox autostart untouched"
fi