diff --git a/claude_desktop/CHANGELOG.md b/claude_desktop/CHANGELOG.md index 255a8f5250..50733300cc 100644 --- a/claude_desktop/CHANGELOG.md +++ b/claude_desktop/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.31 (16-07-2026) + +- Pin the LinuxServer selkies base image to a fixed version (`…-debianbookworm-45960cc3-ls113`) instead of the rolling `…-debianbookworm` tag. The rolling tag is rebuilt continuously (and itself installs selkies "latest" at base-build time), so the desktop/stream runtime could change under the add-on with no change to its own files — builds are now reproducible and the base only moves when this value is bumped deliberately. The pinned tags resolve to exactly the image the rolling tag currently points at (amd64 `sha256:6a4d5154…`, aarch64 `sha256:90914dfd…`). +- Fix Claude Desktop never appearing — the Selkies web client stayed on "waiting for stream" forever with `libEGL warning: failed to open /dev/dri/card0: Permission denied` in the log. The LinuxServer base image grants the desktop user (`abc`) access to the `/dev/dri` render nodes in its `init-video` s6 oneshot, but that oneshot is not a dependency of `svc-xorg`/`svc-selkies`/`svc-de`, so on Home Assistant those long-running services regularly start (via `s6-setuidgid abc`) *before* `abc` has been added to the render group. Xorg/Selkies/pixelflux then open the render device without permission, the video pipeline produces no frames, and the stream never starts. Prepare the exposed DRI nodes in a new `21-gpu_permissions.sh` cont-init script instead: `cont-init.d` runs to completion before any s6-rc service starts, so `abc` is added to each node's owning group (and the node is made world read/write as a timing-independent fallback) in time for the graphical services to use the GPU. Best-effort and a no-op on hosts that expose no GPU. + ## 1.30 (16-07-2026) - Compress large tool outputs automatically in every Claude Code session with a managed `PostToolUse` hook (new `headroom_auto_compress` option, enabled by default). Desktop-spawned sessions (cowork/dispatch) pin `ANTHROPIC_BASE_URL` to the production endpoint (headroom #869), so the transparent proxy never sees their traffic and compression there depended entirely on the model remembering to call the `headroom` MCP tools per the CLAUDE.md guidance — in practice most large outputs went uncompressed. The new `/usr/local/bin/headroom-posttooluse-compress.py` hook fires on `Bash`/`Grep`/`Glob`/`WebFetch` results over ~4000 characters, compresses them with Headroom's rule-based pipeline (SmartCrusher and friends; the Kompress ML path is disabled because its background model load can never complete inside a short-lived hook process), and swaps the result in via `hookSpecificOutput.updatedToolOutput` with a retrieval marker appended. Originals are stored in the shared CCR SQLite store (`~/.headroom/ccr_store.db` — the same one the headroom MCP server reads), so `mcp__headroom__headroom_retrieve` always recovers the full output; savings are recorded to the durable ledger (client `posttooluse-hook`) and show up in the existing gains report. The hook fails open (any error leaves the tool output untouched), never touches `stderr` fields so error text reaches the model verbatim, skips anything below a 50-token savings floor, and is registered idempotently in `~/.claude/settings.json` only after a `--self-test` confirms the interpreter can import headroom; disabling the option (or Headroom) removes the managed entry without touching user-defined hooks. Measured on a representative Home Assistant `states` dump: 10781 -> 2964 tokens (73% saved) at ~1.7 s hook overhead, with sub-100 ms pass-through for small outputs. diff --git a/claude_desktop/build.json b/claude_desktop/build.json index 5ab1765eeb..826ca38614 100644 --- a/claude_desktop/build.json +++ b/claude_desktop/build.json @@ -1,6 +1,6 @@ { "build_from": { - "aarch64": "ghcr.io/linuxserver/baseimage-selkies:arm64v8-debianbookworm", - "amd64": "ghcr.io/linuxserver/baseimage-selkies:amd64-debianbookworm" + "aarch64": "ghcr.io/linuxserver/baseimage-selkies:arm64v8-debianbookworm-45960cc3-ls113", + "amd64": "ghcr.io/linuxserver/baseimage-selkies:amd64-debianbookworm-45960cc3-ls113" } } diff --git a/claude_desktop/config.yaml b/claude_desktop/config.yaml index 52b55e8ab4..46e5af66a1 100644 --- a/claude_desktop/config.yaml +++ b/claude_desktop/config.yaml @@ -111,5 +111,5 @@ slug: claude_desktop tmpfs: true udev: true url: https://github.com/alexbelgium/hassio-addons -version: "1.30" +version: "1.31" video: true diff --git a/claude_desktop/rootfs/etc/cont-init.d/21-gpu_permissions.sh b/claude_desktop/rootfs/etc/cont-init.d/21-gpu_permissions.sh new file mode 100755 index 0000000000..e4fbfe88e8 --- /dev/null +++ b/claude_desktop/rootfs/etc/cont-init.d/21-gpu_permissions.sh @@ -0,0 +1,52 @@ +#!/usr/bin/with-contenv bashio +# shellcheck shell=bash +set -e + +# Grant the shared desktop user (abc) access to the exposed GPU render nodes *before* the +# graphical services start. +# +# The LinuxServer base image already sets up /dev/dri group access, but it does so in its +# init-video s6 oneshot, which is NOT a dependency of svc-xorg/svc-selkies/svc-de. On Home +# Assistant those long-running services routinely start (as abc, via s6-setuidgid) before +# init-video has added abc to the render group, so Xorg/Selkies/pixelflux open the render +# device with the wrong credentials and fail: +# +# libEGL warning: failed to open /dev/dri/card0: Permission denied +# +# With no usable render node the video pipeline never produces frames, so the Selkies web +# client stays on "waiting for stream" indefinitely and Claude Desktop never appears. +# +# cont-init.d runs to completion before any s6-rc service is started, so preparing the DRI +# nodes here wins that race. Everything is best-effort: a host that exposes no GPU simply has +# no nodes to touch and this is a no-op. + +shopt -s nullglob +dri_nodes=(/dev/dri/card* /dev/dri/render*) +if [ "${#dri_nodes[@]}" -eq 0 ]; then + bashio::log.info "No /dev/dri render nodes exposed; skipping GPU permission setup" + exit 0 +fi + +for node in "${dri_nodes[@]}"; do + [ -e "$node" ] || continue + + # Mirror the base image's init-video logic (add abc to the node's owning group, creating + # 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}')" + if [ -z "$gname" ]; then + gname="dri${gid}" + groupadd -o -g "$gid" "$gname" 2> /dev/null || true + fi + if ! id -G abc 2> /dev/null | tr ' ' '\n' | grep -qx "$gid"; then + usermod -a -G "$gname" abc 2> /dev/null || true + fi + + # Guarantee access even where group propagation is unreliable inside the add-on sandbox: + # this is a single-user desktop container, so world read/write on the local render node + # is acceptable and removes any dependency on group-membership timing. + chmod o+rw "$node" 2> /dev/null || true + + bashio::log.info "GPU: prepared ${node} (group ${gname}:${gid}) for the desktop user" +done