diff --git a/claude_desktop/CHANGELOG.md b/claude_desktop/CHANGELOG.md index 783f74f10a..b15957c674 100644 --- a/claude_desktop/CHANGELOG.md +++ b/claude_desktop/CHANGELOG.md @@ -1,3 +1,12 @@ +## 1.26 (15-07-2026) + +- Fix startup permission failures that prevented Claude Desktop from starting: storage was chowned to a hardcoded `1000:1000`, but the shared `abc` desktop user was never mapped to that UID. During init `abc` was still the image default (`911`), so TokenSave (`.claude.json.new`), RTK (`RTK.md`), nginx, PulseAudio, the Mesa shader cache, and Claude Desktop itself all hit `Permission denied`; the base image's `init-adduser` then remapped `abc` to root mid-startup (PUID/PGID were read from add-on options where they did not exist, falling back to `0`), which also made Claude Code reject `permission_mode: bypass`. +- Add `PUID`/`PGID` add-on options (default `1000:1000`) and remap `abc` to that identity at the very start of folder setup, before any ownership is applied and before any service resolves the user. The base image's `init-adduser` is pinned to the same effective identity so it can no longer remap `abc` mid-startup. +- In `permission_mode: bypass`, a configured `PUID: 0` automatically falls back to UID `1000` (Claude Code refuses bypass permissions as root), retaining the configured group. +- Fix `bashio::config.array: command not found` in the TokenSave repository setup, tools configuration, and `claude-tools-doctor.sh`: the function only exists in the repo's standalone bashio, not in the real bashio shipped in the image. Use `bashio::config`, which prints list entries one per line. +- Return managed Claude configuration files to the effective `abc` identity instead of the raw configured `PUID`/`PGID` (which previously fell back to `0` and left the files root-owned). +- Pre-create `/tmp/.X11-unix` with the standard sticky mode so Xorg, which runs as the non-root `abc` user on a tmpfs `/tmp`, no longer fails to create its socket directory (`_XSERVTransmkdir: euid != 0`). + ## 1.25 (15-07-2026) - Minor bugs fixed ## 1.24 (15-07-2026) diff --git a/claude_desktop/README.md b/claude_desktop/README.md index 50c138bfb2..6e2e59c5be 100644 --- a/claude_desktop/README.md +++ b/claude_desktop/README.md @@ -35,9 +35,10 @@ PATH tools. `/usr/bin/claude` directly, the session remains functional and still has the shared permission mode and Headroom MCP tools, but transparent proxy compression cannot be injected. -- When `permission_mode: bypass` is selected while `PUID` is `0`, the add-on - automatically remaps the shared `abc` desktop account to an unused non-root - UID before Selkies and Claude Desktop start. Claude Code refuses bypass mode +- The shared `abc` desktop account runs under the configured `PUID`/`PGID` + (default `1000:1000`). When `permission_mode: bypass` is selected while + `PUID` is `0`, the add-on automatically falls back to UID `1000` before + Selkies and Claude Desktop start, because Claude Code refuses bypass mode under an effective root UID. - **gnome-keyring** provides the Secret Service backend Electron needs to persist sign-in and dispatch permission grants across restarts. @@ -88,7 +89,7 @@ Git synchronization hooks. A repository is indexed only when it is listed in | Option | Default | Description | | ------ | ------- | ----------- | -| `PUID` / `PGID` | `0` / `0` | Numeric user and group applied by LinuxServer initialization. In bypass mode, a root `PUID` is automatically replaced at runtime by an unused non-root UID while the configured group is retained. | +| `PUID` / `PGID` | `1000` / `1000` | Numeric user and group of the shared `abc` desktop account that owns the data location and runs Claude Desktop. In bypass mode, a root `PUID` is automatically replaced at runtime by UID `1000` while the configured group is retained. | | `TZ` | | Optional timezone, for example `Europe/Brussels`. | | `KEYBOARD` | | Optional Selkies keyboard layout. | | `PASSWORD` | | Optional password for direct Selkies ports. | @@ -131,11 +132,11 @@ permission_mode: auto `--dangerously-skip-permissions` for wrapper-launched sessions. Claude Code does not permit bypass mode when its effective UID is `0`. If the -add-on is configured with `PUID: 0`, selecting `bypass` remaps only the shared -`abc` runtime account to an available non-root UID (preferring `1000`, then -`911`) before storage ownership and Desktop startup. Its configured primary -GID is retained, so group-based access to mounted Home Assistant paths remains -available. Strict and auto modes keep the configured identity unchanged. +add-on is configured with `PUID: 0`, selecting `bypass` runs the shared `abc` +runtime account as UID `1000` instead, before storage ownership and Desktop +startup. Its configured primary GID is retained, so group-based access to +mounted Home Assistant paths remains available. Strict and auto modes keep the +configured identity unchanged. A root shell invoking `/usr/local/bin/claude` in bypass mode is also dropped to the remapped `abc` account. Directly invoking `/usr/bin/claude` as root still diff --git a/claude_desktop/config.yaml b/claude_desktop/config.yaml index 62da971837..0cfdfb153d 100644 --- a/claude_desktop/config.yaml +++ b/claude_desktop/config.yaml @@ -34,6 +34,8 @@ name: Claude Desktop options: env_vars: [] DNS_server: 8.8.8.8 + PGID: 1000 + PUID: 1000 data_location: /data/data additional_apps: "" additional_pip: "" @@ -75,6 +77,8 @@ schema: DRINODE: list(/dev/dri/card0|/dev/dri/card1|/dev/dri/card2|/dev/dri/renderD128|/dev/dri/renderD129|)? KEYBOARD: list(da-dk-qwerty|de-de-qwertz|en-gb-qwerty|en-us-qwerty|es-es-qwerty|fr-ch-qwertz|fr-fr-azerty|it-it-qwerty|ja-jp-qwerty|pt-br-qwerty|sv-se-qwerty|tr-tr-qwerty)? PASSWORD: str? + PGID: int + PUID: int TZ: match([A-Z][a-z]*./[A-Z][a-z]*.)? additional_apps: str? additional_pip: str? @@ -105,5 +109,5 @@ slug: claude_desktop tmpfs: true udev: true url: https://github.com/alexbelgium/hassio-addons -version: "1.25" +version: "1.26" video: true diff --git a/claude_desktop/rootfs/etc/cont-init.d/20-folders.sh b/claude_desktop/rootfs/etc/cont-init.d/20-folders.sh index f9b93ac23e..6ef48206a9 100755 --- a/claude_desktop/rootfs/etc/cont-init.d/20-folders.sh +++ b/claude_desktop/rootfs/etc/cont-init.d/20-folders.sh @@ -3,10 +3,35 @@ # shellcheck disable=SC2046 set -e -# Use the effective shared desktop user identity. In bypass mode an earlier init script may -# remap abc away from UID 0 because Claude Code rejects bypass permissions when run as root. -PUID=1000 -PGID=1000 +# Align the shared desktop user (abc) with the configured PUID/PGID before any storage is +# chowned and before any service or s6-setuidgid call resolves abc. The base image's +# init-adduser applies the same remap, but it runs after cont-init, so doing it here first is +# what lets the tokensave/rtk/git setup in the 8x scripts run under the final identity. +PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else echo '1000'; fi)" +PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '1000'; fi)" + +# Claude Code refuses bypass-permissions mode under an effective root UID, so bypass mode +# always needs a non-root desktop user. +if [ "$(bashio::config 'permission_mode')" = "bypass" ] && [ "$PUID" -eq 0 ]; then + bashio::log.warning "permission_mode: bypass cannot run Claude Code as root; using UID 1000 instead of the configured PUID 0" + PUID=1000 +fi + +groupmod -o -g "$PGID" abc 2> /dev/null || true +usermod -o -u "$PUID" abc 2> /dev/null || true +if [ "$(id -u abc)" -ne "$PUID" ] || [ "$(id -g abc)" -ne "$PGID" ]; then + PUID="$(id -u abc)" + PGID="$(id -g abc)" + bashio::log.warning "Unable to remap the abc desktop user; continuing with its current identity ${PUID}:${PGID}" +fi + +# The base image's init-adduser reads PUID/PGID from the raw add-on options (default 0) and +# runs mid-startup, racing the services. Pin it to the effective identity chosen above so it +# can never remap abc away from the ownership applied below. +ADDUSER_RUN="/etc/s6-overlay/s6-rc.d/init-adduser/run" +if [ -f "$ADDUSER_RUN" ]; then + sed -i "s|^PUID=.*|PUID=${PUID}|;s|^PGID=.*|PGID=${PGID}|" "$ADDUSER_RUN" +fi # Check data location LOCATION="$(bashio::config 'data_location')" @@ -71,6 +96,11 @@ mkdir -p "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR" chmod 755 /tmp/cache chmod 700 "$XDG_RUNTIME_DIR" +# /tmp is a tmpfs and Xorg runs as the non-root abc user, which cannot create the X11 socket +# directory itself (_XSERVTransmkdir: euid != 0). Pre-create it with the standard sticky mode. +mkdir -p /tmp/.X11-unix +chmod 1777 /tmp/.X11-unix + # Pre-create the Selkies joystick log so the base image's "chmod 777 /tmp/selkies*" # calls (in init-selkies-config and svc-de) never fail on an empty glob. touch /tmp/selkies_js.log @@ -82,7 +112,7 @@ fi ln -sfn /tmp/cache "$LOCATION/.cache" bashio::log.info "Setting ownership to $PUID:$PGID" -chown -R abc "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR" /data +chown -R "${PUID}:${PGID}" "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR" /data chmod -R 700 "$LOCATION" # The base init-selkies-config script overrides XDG_RUNTIME_DIR to $HOME/.XDG, which lands diff --git a/claude_desktop/rootfs/etc/cont-init.d/81-tokensave_repositories.sh b/claude_desktop/rootfs/etc/cont-init.d/81-tokensave_repositories.sh index 4b36dbc397..c9ee94147f 100755 --- a/claude_desktop/rootfs/etc/cont-init.d/81-tokensave_repositories.sh +++ b/claude_desktop/rootfs/etc/cont-init.d/81-tokensave_repositories.sh @@ -11,7 +11,7 @@ declare -A REPOS_SEEN=() while IFS= read -r configured_path; do configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}" configured_path="${configured_path%"${configured_path##*[![:space:]]}"}" - [ -n "$configured_path" ] || continue + [ -n "$configured_path" ] && [ "$configured_path" != "null" ] || continue case "$configured_path" in /*) ;; @@ -33,4 +33,6 @@ while IFS= read -r configured_path; do s6-setuidgid abc env HOME="$HOME" git config --global --add safe.directory "$repo_root" bashio::log.info "Marked TokenSave repository as safe for Git: ${repo_root}" fi -done < <(bashio::config.array 'tokensave_project_paths') +# bashio::config prints list options one entry per line ("null" when the key is absent); +# bashio::config.array only exists in the repo's standalone bashio, not in the real bashio here. +done < <(bashio::config 'tokensave_project_paths') diff --git a/claude_desktop/rootfs/etc/cont-init.d/82-claude_tools.sh b/claude_desktop/rootfs/etc/cont-init.d/82-claude_tools.sh index be0f65cca4..b6c6e3cee1 100755 --- a/claude_desktop/rootfs/etc/cont-init.d/82-claude_tools.sh +++ b/claude_desktop/rootfs/etc/cont-init.d/82-claude_tools.sh @@ -3,8 +3,10 @@ set -e set -o pipefail -PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else echo '0'; fi)" -PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '0'; fi)" +# 20-folders.sh already remapped abc to the effective runtime identity (never root in bypass +# mode), so follow abc instead of re-reading the raw PUID/PGID options here. +RUNTIME_UID="$(id -u abc)" +RUNTIME_GID="$(id -g abc)" mkdir -p "$HOME/.claude" run_as_runtime_user() { @@ -169,7 +171,7 @@ if $TOKENSAVE_ENABLED; then # Trim surrounding whitespace while preserving spaces inside paths. configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}" configured_path="${configured_path%"${configured_path##*[![:space:]]}"}" - [ -n "$configured_path" ] || continue + [ -n "$configured_path" ] && [ "$configured_path" != "null" ] || continue case "$configured_path" in /*) ;; @@ -251,7 +253,9 @@ if $TOKENSAVE_ENABLED; then exit 1 ' _ "$repo_root" \ || bashio::log.warning "TokenSave preparation failed for ${repo_root}" - done < <(bashio::config.array 'tokensave_project_paths') + # bashio::config prints list options one entry per line ("null" when the key is absent); + # bashio::config.array only exists in the repo's standalone bashio, not in the real bashio here. + done < <(bashio::config 'tokensave_project_paths') fi # Guide Claude to actually use the Headroom compression tools so the MCP integration produces @@ -461,9 +465,9 @@ else fi # Startup configuration runs as root, while Claude Desktop runs as abc. Return managed -# persistent files to the configured runtime UID/GID after all writes complete. +# persistent files to the effective runtime UID/GID after all writes complete. for managed_path in "$HOME/.claude" "$HOME/.claude.json" "$HOME/.config/Claude"; do if [ -e "$managed_path" ]; then - chown -R -- "${PUID}:${PGID}" "$managed_path" || bashio::log.warning "Unable to set ownership on $managed_path" + chown -R -- "${RUNTIME_UID}:${RUNTIME_GID}" "$managed_path" || bashio::log.warning "Unable to set ownership on $managed_path" fi done diff --git a/claude_desktop/rootfs/etc/cont-init.d/83-claude_permissions.sh b/claude_desktop/rootfs/etc/cont-init.d/83-claude_permissions.sh index 11c3affb22..99df9ed7e5 100755 --- a/claude_desktop/rootfs/etc/cont-init.d/83-claude_permissions.sh +++ b/claude_desktop/rootfs/etc/cont-init.d/83-claude_permissions.sh @@ -3,8 +3,10 @@ set -e set -o pipefail -PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else echo '0'; fi)" -PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '0'; fi)" +# 20-folders.sh already remapped abc to the effective runtime identity (never root in bypass +# mode), so follow abc instead of re-reading the raw PUID/PGID options here. +RUNTIME_UID="$(id -u abc)" +RUNTIME_GID="$(id -g abc)" PERMISSION_MODE="$(bashio::config 'permission_mode')" SETTINGS_PATH="$HOME/.claude/settings.json" STATE_PATH="$HOME/.claude/.addon-permission-mode.json" @@ -86,7 +88,7 @@ case "$PERMISSION_MODE" in ;; esac -chown -- "${PUID}:${PGID}" "$SETTINGS_PATH" 2> /dev/null || true +chown -- "${RUNTIME_UID}:${RUNTIME_GID}" "$SETTINGS_PATH" 2> /dev/null || true if [ -e "$STATE_PATH" ]; then - chown -- "${PUID}:${PGID}" "$STATE_PATH" 2> /dev/null || true + chown -- "${RUNTIME_UID}:${RUNTIME_GID}" "$STATE_PATH" 2> /dev/null || true fi diff --git a/claude_desktop/rootfs/etc/cont-init.d/84-claude_runtime_ownership.sh b/claude_desktop/rootfs/etc/cont-init.d/84-claude_runtime_ownership.sh index 45402914f4..61126da708 100755 --- a/claude_desktop/rootfs/etc/cont-init.d/84-claude_runtime_ownership.sh +++ b/claude_desktop/rootfs/etc/cont-init.d/84-claude_runtime_ownership.sh @@ -2,10 +2,10 @@ # shellcheck shell=bash set -e -# Earlier configuration scripts intentionally run as root and may use the configured PUID/PGID -# values when returning files to the runtime user. In bypass mode PUID can still be configured as -# 0 even though 19-claude_bypass_runtime.sh remapped abc to a non-root UID. Reconcile ownership -# with the effective desktop identity after all Claude configuration writes are complete. +# Earlier configuration scripts intentionally run as root. 20-folders.sh remapped abc to the +# effective runtime identity (never root in bypass mode, where Claude Code refuses to run as +# root). Reconcile ownership with that identity after all Claude configuration writes are +# complete, as a safety net in case any intermediate step re-owned a managed path. RUNTIME_UID="$(id -u abc)" RUNTIME_GID="$(id -g abc)" diff --git a/claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh b/claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh index 150b73f46d..d34647c461 100755 --- a/claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh +++ b/claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh @@ -150,7 +150,7 @@ if bashio::config.true 'install_tokensave'; then tokensave doctor --agent claude || true tokensave gain --all --range 30d || true while IFS= read -r configured_path; do - [ -n "$configured_path" ] || continue + [ -n "$configured_path" ] && [ "$configured_path" != "null" ] || continue repo_root="$(s6-setuidgid abc env HOME="$HOME" git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)" if [ -z "$repo_root" ]; then echo "${configured_path}: not a Git repository" @@ -159,7 +159,7 @@ if bashio::config.true 'install_tokensave'; then else echo "${repo_root}: NOT INITIALIZED" fi - done < <(bashio::config.array 'tokensave_project_paths') + done < <(bashio::config 'tokensave_project_paths') else echo "disabled" fi