Merge pull request #2868 from alexbelgium/claude/claude-desktop-permissions-cm4k7w

fix(claude_desktop): align abc runtime identity so Claude Desktop can start
This commit is contained in:
Alexandre
2026-07-15 19:15:23 +02:00
committed by GitHub
9 changed files with 98 additions and 36 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

@@ -8,10 +8,14 @@ if ! bashio::config.true 'install_tokensave' || ! command -v git > /dev/null 2>&
fi fi
declare -A REPOS_SEEN=() declare -A REPOS_SEEN=()
while IFS= read -r configured_path; do # bashio::config prints its result without a trailing newline, so the last record arrives
# with read returning non-zero; the extra test keeps that final path in the loop.
while IFS= read -r configured_path || [ -n "$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 if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
continue
fi
case "$configured_path" in case "$configured_path" in
/*) ;; /*) ;;
@@ -33,4 +37,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() {
@@ -165,11 +167,15 @@ PY
# requires one-time per-project opt-in; an empty list therefore has no startup or storage cost. # requires one-time per-project opt-in; an empty list therefore has no startup or storage cost.
if $TOKENSAVE_ENABLED; then if $TOKENSAVE_ENABLED; then
declare -A TOKENSAVE_REPOS_SEEN=() declare -A TOKENSAVE_REPOS_SEEN=()
while IFS= read -r configured_path; do # bashio::config prints its result without a trailing newline, so the last record arrives
# with read returning non-zero; the extra test keeps that final path in the loop.
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
# 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 if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
continue
fi
case "$configured_path" in case "$configured_path" in
/*) ;; /*) ;;
@@ -251,7 +257,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 +469,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

@@ -149,8 +149,10 @@ section "TokenSave"
if bashio::config.true 'install_tokensave'; then 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 || [ -n "$configured_path" ]; do
[ -n "$configured_path" ] || continue if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
continue
fi
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 +161,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