fix(claude_desktop): align abc runtime identity so Claude Desktop can start

Version 1.25 chowned the data location to a hardcoded 1000:1000 but never
mapped the shared abc desktop user to that UID: during cont-init abc was
still the image default (911), so TokenSave, RTK, nginx, PulseAudio, the
Mesa shader cache, and Claude Desktop itself failed with Permission denied.
The base image's init-adduser then remapped abc to root mid-startup because
it reads PUID/PGID from add-on options (fallback 0) where they were never
defined, which additionally made Claude Code reject bypass mode.

- Add PUID/PGID add-on options (default 1000:1000) and remap abc to that
  identity at the top of 20-folders.sh, before any ownership pass and
  before any service resolves the user; pin init-adduser to the same
  effective identity so it cannot diverge mid-startup.
- In permission_mode bypass, fall back from a configured PUID 0 to UID
  1000, since Claude Code refuses bypass permissions as root.
- Replace the nonexistent bashio::config.array (only present in the repo's
  standalone bashio) with bashio::config in the TokenSave repository setup,
  tools configuration, and claude-tools-doctor.sh.
- Chown managed Claude configuration files to the effective abc identity
  instead of the raw configured PUID/PGID, which fell back to root.
- Pre-create /tmp/.X11-unix (sticky 1777) so Xorg running as non-root abc
  on the tmpfs /tmp can create its socket.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KN8i26JrKSaBdvTrpVEyQ6
This commit is contained in:
Claude
2026-07-15 16:17:53 +00:00
parent 26c26e3668
commit 0cfe28a405
9 changed files with 85 additions and 33 deletions

View File

@@ -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) ## 1.25 (15-07-2026)
- Minor bugs fixed - Minor bugs fixed
## 1.24 (15-07-2026) ## 1.24 (15-07-2026)

View File

@@ -35,9 +35,10 @@ PATH tools.
`/usr/bin/claude` directly, the session remains functional and still has the `/usr/bin/claude` directly, the session remains functional and still has the
shared permission mode and Headroom MCP tools, but transparent proxy shared permission mode and Headroom MCP tools, but transparent proxy
compression cannot be injected. compression cannot be injected.
- When `permission_mode: bypass` is selected while `PUID` is `0`, the add-on - The shared `abc` desktop account runs under the configured `PUID`/`PGID`
automatically remaps the shared `abc` desktop account to an unused non-root (default `1000:1000`). When `permission_mode: bypass` is selected while
UID before Selkies and Claude Desktop start. Claude Code refuses bypass mode `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. under an effective root UID.
- **gnome-keyring** provides the Secret Service backend Electron needs to - **gnome-keyring** provides the Secret Service backend Electron needs to
persist sign-in and dispatch permission grants across restarts. 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 | | 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`. | | `TZ` | | Optional timezone, for example `Europe/Brussels`. |
| `KEYBOARD` | | Optional Selkies keyboard layout. | | `KEYBOARD` | | Optional Selkies keyboard layout. |
| `PASSWORD` | | Optional password for direct Selkies ports. | | `PASSWORD` | | Optional password for direct Selkies ports. |
@@ -131,11 +132,11 @@ permission_mode: auto
`--dangerously-skip-permissions` for wrapper-launched sessions. `--dangerously-skip-permissions` for wrapper-launched sessions.
Claude Code does not permit bypass mode when its effective UID is `0`. If the 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 add-on is configured with `PUID: 0`, selecting `bypass` runs the shared `abc`
`abc` runtime account to an available non-root UID (preferring `1000`, then runtime account as UID `1000` instead, before storage ownership and Desktop
`911`) before storage ownership and Desktop startup. Its configured primary startup. Its configured primary GID is retained, so group-based access to
GID is retained, so group-based access to mounted Home Assistant paths remains mounted Home Assistant paths remains available. Strict and auto modes keep the
available. Strict and auto modes keep the configured identity unchanged. configured identity unchanged.
A root shell invoking `/usr/local/bin/claude` in bypass mode is also dropped to 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 the remapped `abc` account. Directly invoking `/usr/bin/claude` as root still

View File

@@ -34,6 +34,8 @@ name: Claude Desktop
options: options:
env_vars: [] env_vars: []
DNS_server: 8.8.8.8 DNS_server: 8.8.8.8
PGID: 1000
PUID: 1000
data_location: /data/data data_location: /data/data
additional_apps: "" additional_apps: ""
additional_pip: "" additional_pip: ""
@@ -75,6 +77,8 @@ schema:
DRINODE: list(/dev/dri/card0|/dev/dri/card1|/dev/dri/card2|/dev/dri/renderD128|/dev/dri/renderD129|)? 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)? 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? PASSWORD: str?
PGID: int
PUID: int
TZ: match([A-Z][a-z]*./[A-Z][a-z]*.)? TZ: match([A-Z][a-z]*./[A-Z][a-z]*.)?
additional_apps: str? additional_apps: str?
additional_pip: str? additional_pip: str?
@@ -105,5 +109,5 @@ slug: claude_desktop
tmpfs: true tmpfs: true
udev: true udev: true
url: https://github.com/alexbelgium/hassio-addons url: https://github.com/alexbelgium/hassio-addons
version: "1.25" version: "1.26"
video: true video: true

View File

@@ -3,10 +3,35 @@
# shellcheck disable=SC2046 # shellcheck disable=SC2046
set -e set -e
# Use the effective shared desktop user identity. In bypass mode an earlier init script may # Align the shared desktop user (abc) with the configured PUID/PGID before any storage is
# remap abc away from UID 0 because Claude Code rejects bypass permissions when run as root. # 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 PUID=1000
PGID=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 # Check data location
LOCATION="$(bashio::config 'data_location')" LOCATION="$(bashio::config 'data_location')"
@@ -71,6 +96,11 @@ mkdir -p "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR"
chmod 755 /tmp/cache chmod 755 /tmp/cache
chmod 700 "$XDG_RUNTIME_DIR" 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*" # 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. # calls (in init-selkies-config and svc-de) never fail on an empty glob.
touch /tmp/selkies_js.log touch /tmp/selkies_js.log
@@ -82,7 +112,7 @@ fi
ln -sfn /tmp/cache "$LOCATION/.cache" ln -sfn /tmp/cache "$LOCATION/.cache"
bashio::log.info "Setting ownership to $PUID:$PGID" 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" chmod -R 700 "$LOCATION"
# The base init-selkies-config script overrides XDG_RUNTIME_DIR to $HOME/.XDG, which lands # The base init-selkies-config script overrides XDG_RUNTIME_DIR to $HOME/.XDG, which lands

View File

@@ -11,7 +11,7 @@ declare -A REPOS_SEEN=()
while IFS= read -r configured_path; do while IFS= read -r configured_path; do
configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}" configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}"
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 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" 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}" bashio::log.info "Marked TokenSave repository as safe for Git: ${repo_root}"
fi 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')

View File

@@ -3,8 +3,10 @@
set -e set -e
set -o pipefail set -o pipefail
PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else echo '0'; fi)" # 20-folders.sh already remapped abc to the effective runtime identity (never root in bypass
PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '0'; fi)" # 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" mkdir -p "$HOME/.claude"
run_as_runtime_user() { run_as_runtime_user() {
@@ -169,7 +171,7 @@ if $TOKENSAVE_ENABLED; then
# Trim surrounding whitespace while preserving spaces inside paths. # Trim surrounding whitespace while preserving spaces inside paths.
configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}" configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}"
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 case "$configured_path" in
/*) ;; /*) ;;
@@ -251,7 +253,9 @@ if $TOKENSAVE_ENABLED; then
exit 1 exit 1
' _ "$repo_root" \ ' _ "$repo_root" \
|| bashio::log.warning "TokenSave preparation failed for ${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 fi
# Guide Claude to actually use the Headroom compression tools so the MCP integration produces # Guide Claude to actually use the Headroom compression tools so the MCP integration produces
@@ -461,9 +465,9 @@ else
fi fi
# Startup configuration runs as root, while Claude Desktop runs as abc. Return managed # 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 for managed_path in "$HOME/.claude" "$HOME/.claude.json" "$HOME/.config/Claude"; do
if [ -e "$managed_path" ]; then 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 fi
done done

View File

@@ -3,8 +3,10 @@
set -e set -e
set -o pipefail set -o pipefail
PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else echo '0'; fi)" # 20-folders.sh already remapped abc to the effective runtime identity (never root in bypass
PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '0'; fi)" # 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')" PERMISSION_MODE="$(bashio::config 'permission_mode')"
SETTINGS_PATH="$HOME/.claude/settings.json" SETTINGS_PATH="$HOME/.claude/settings.json"
STATE_PATH="$HOME/.claude/.addon-permission-mode.json" STATE_PATH="$HOME/.claude/.addon-permission-mode.json"
@@ -86,7 +88,7 @@ case "$PERMISSION_MODE" in
;; ;;
esac 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 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 fi

View File

@@ -2,10 +2,10 @@
# shellcheck shell=bash # shellcheck shell=bash
set -e set -e
# Earlier configuration scripts intentionally run as root and may use the configured PUID/PGID # Earlier configuration scripts intentionally run as root. 20-folders.sh remapped abc to the
# values when returning files to the runtime user. In bypass mode PUID can still be configured as # effective runtime identity (never root in bypass mode, where Claude Code refuses to run as
# 0 even though 19-claude_bypass_runtime.sh remapped abc to a non-root UID. Reconcile ownership # root). Reconcile ownership with that identity after all Claude configuration writes are
# with the effective desktop identity after all Claude configuration writes are complete. # complete, as a safety net in case any intermediate step re-owned a managed path.
RUNTIME_UID="$(id -u abc)" RUNTIME_UID="$(id -u abc)"
RUNTIME_GID="$(id -g abc)" RUNTIME_GID="$(id -g abc)"

View File

@@ -150,7 +150,7 @@ if bashio::config.true 'install_tokensave'; then
tokensave doctor --agent claude || true tokensave doctor --agent claude || true
tokensave gain --all --range 30d || true tokensave gain --all --range 30d || true
while IFS= read -r configured_path; do 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)" 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 if [ -z "$repo_root" ]; then
echo "${configured_path}: not a Git repository" echo "${configured_path}: not a Git repository"
@@ -159,7 +159,7 @@ if bashio::config.true 'install_tokensave'; then
else else
echo "${repo_root}: NOT INITIALIZED" echo "${repo_root}: NOT INITIALIZED"
fi fi
done < <(bashio::config.array 'tokensave_project_paths') done < <(bashio::config 'tokensave_project_paths')
else else
echo "disabled" echo "disabled"
fi fi