diff --git a/claude_desktop/CHANGELOG.md b/claude_desktop/CHANGELOG.md index 098c4420bf..5757b7603d 100644 --- a/claude_desktop/CHANGELOG.md +++ b/claude_desktop/CHANGELOG.md @@ -1,3 +1,11 @@ +## 1.21 (15-07-2026) + +- Fix Claude Code bypass permissions being rejected when the add-on uses its default root `PUID`. +- In `permission_mode: bypass`, remap the shared `abc` Desktop runtime to an unused non-root UID before storage ownership and Selkies startup, while retaining its configured primary group for mounted-path access. +- Make folder setup and final Claude configuration ownership follow the effective `abc` identity instead of the configured root UID. +- Drop root console invocations of the add-on's `/usr/local/bin/claude` wrapper to the non-root `abc` runtime before passing `--dangerously-skip-permissions`. +- Extend `claude-tools-doctor.sh` with configured/effective UID and GID checks for bypass mode. + ## 1.20 (15-07-2026) - Complete the TokenSave Claude Code integration at startup: install its MCP server, permissions, PreToolUse/UserPromptSubmit/Stop hooks, global guidance, and Git synchronization hooks instead of registering only `tokensave serve`. diff --git a/claude_desktop/README.md b/claude_desktop/README.md index 33cf4208e7..fd5600d25b 100644 --- a/claude_desktop/README.md +++ b/claude_desktop/README.md @@ -35,6 +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 + under an effective root UID. - **gnome-keyring** provides the Secret Service backend Electron needs to persist sign-in and dispatch permission grants across restarts. @@ -63,6 +67,8 @@ Git synchronization hooks. A repository is indexed only when it is listed in - Persistent sign-in through a bundled, auto-unlocked gnome-keyring. - Configurable Claude Code permissions: strict prompts, automatic safe-action approval, or explicit full bypass for trusted installations. +- Automatic non-root runtime enforcement for bypass mode, including root-console + wrapper launches. - Optional runtime Claude Desktop updates from Anthropic's apt repository. - Optional extra apt and pip package installation (pip installs use `uv`). - Baked-in `git`, GitHub CLI (`gh`), `ripgrep`, `jq`, `shellcheck`, `yamllint`, @@ -74,14 +80,15 @@ Git synchronization hooks. A repository is indexed only when it is listed in Assistant. - Independent hourly savings reports for Headroom, RTK, and TokenSave. - `claude-tools-doctor.sh` diagnostics for binaries, routing, hooks, MCP - registrations, project indexes, proxy health, permissions, and gains. + registrations, project indexes, proxy health, permissions, runtime identity, + and gains. - Low-power defaults for GPU mapping, Selkies frame rate, and volatile caches. ## Options | Option | Default | Description | | ------ | ------- | ----------- | -| `PUID` / `PGID` | `0` / `0` | Numeric user and group applied by the LinuxServer initialization. | +| `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. | | `TZ` | | Optional timezone, for example `Europe/Brussels`. | | `KEYBOARD` | | Optional Selkies keyboard layout. | | `PASSWORD` | | Optional password for direct Selkies ports. | @@ -122,9 +129,21 @@ permission_mode: auto `bypassPermissions` in the shared settings and `--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. + +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 +bypasses the add-on wrapper and will be rejected by Claude Code. + `bypass` gives Claude broad authority over all mounted writable data and every command or credential available inside the add-on. Enable it only in a trusted -installation with trusted repositories and mounts. +installation with trusted repositories and mounts. Mounted paths must remain +accessible to the effective non-root UID or its retained group. ### TokenSave project example @@ -171,10 +190,11 @@ Run the following inside the add-on through a custom script or container console claude-tools-doctor.sh ``` -The report checks the tool binaries, configuration switches, redacted MCP -registrations, Claude hooks, permission mode, Headroom health, TokenSave indexes, -routing, and recorded savings. It never prints MCP environment values because -the Home Assistant MCP entry can contain a long-lived token. +The report checks the tool binaries, configuration switches, configured and +effective runtime identities, redacted MCP registrations, Claude hooks, +permission mode, Headroom health, TokenSave indexes, routing, and recorded +savings. It never prints MCP environment values because the Home Assistant MCP +entry can contain a long-lived token. The hourly report can also be invoked manually: diff --git a/claude_desktop/config.yaml b/claude_desktop/config.yaml index b54e4f4ae7..edeea5a405 100644 --- a/claude_desktop/config.yaml +++ b/claude_desktop/config.yaml @@ -103,5 +103,5 @@ slug: claude_desktop tmpfs: true udev: true url: https://github.com/alexbelgium/hassio-addons -version: "1.20" +version: "1.21" video: true diff --git a/claude_desktop/rootfs/etc/cont-init.d/19-claude_bypass_runtime.sh b/claude_desktop/rootfs/etc/cont-init.d/19-claude_bypass_runtime.sh new file mode 100644 index 0000000000..c55d307825 --- /dev/null +++ b/claude_desktop/rootfs/etc/cont-init.d/19-claude_bypass_runtime.sh @@ -0,0 +1,48 @@ +#!/usr/bin/with-contenv bashio +# shellcheck shell=bash +set -e +set -o pipefail + +# Claude Code deliberately refuses bypass-permissions mode when its effective UID is 0. +# The add-on historically defaults PUID to 0, so switch the shared `abc` desktop user to +# an unused non-root UID before storage ownership and Selkies runtime directories are set up. +# Keep abc's configured primary group (commonly group 0) so existing group-based access to +# Home Assistant mounts is preserved. Strict and auto permission modes are unchanged. +if [ "$(bashio::config 'permission_mode')" != "bypass" ]; then + exit 0 +fi + +CURRENT_UID="$(id -u abc)" +if [ "$CURRENT_UID" -ne 0 ]; then + bashio::log.info "Claude bypass runtime already uses non-root UID ${CURRENT_UID}" + exit 0 +fi + +find_available_uid() { + local candidate owner + for candidate in 1000 911 $(seq 1001 1099); do + owner="$(getent passwd "$candidate" | cut -d: -f1 || true)" + if [ -z "$owner" ] || [ "$owner" = "abc" ]; then + printf '%s' "$candidate" + return 0 + fi + done + return 1 +} + +TARGET_UID="$(find_available_uid || true)" +if [ -z "$TARGET_UID" ]; then + bashio::exit.nok "Claude bypass mode requires a non-root runtime user, but no free fallback UID was found" +fi + +usermod --uid "$TARGET_UID" abc + +if [ "$(id -u abc)" -eq 0 ]; then + bashio::exit.nok "Unable to switch the Claude Desktop runtime away from root for bypass mode" +fi + +mkdir -p /run/s6/container_environment +printf '%s' "$TARGET_UID" > /run/s6/container_environment/CLAUDE_RUNTIME_UID +printf '%s' "$(id -g abc)" > /run/s6/container_environment/CLAUDE_RUNTIME_GID + +bashio::log.warning "Claude bypass mode cannot run as root; remapped abc from UID 0 to UID ${TARGET_UID} (GID $(id -g abc))" 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 cbcae8160e..6f936d26e0 100755 --- a/claude_desktop/rootfs/etc/cont-init.d/20-folders.sh +++ b/claude_desktop/rootfs/etc/cont-init.d/20-folders.sh @@ -3,9 +3,10 @@ # shellcheck disable=SC2046 set -e -# Define user -PUID=$(bashio::config "PUID") -PGID=$(bashio::config "PGID") +# 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="$(id -u abc)" +PGID="$(id -g abc)" # Check data location LOCATION="$(bashio::config 'data_location')" 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 new file mode 100644 index 0000000000..45402914f4 --- /dev/null +++ b/claude_desktop/rootfs/etc/cont-init.d/84-claude_runtime_ownership.sh @@ -0,0 +1,17 @@ +#!/usr/bin/with-contenv bashio +# 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. +RUNTIME_UID="$(id -u abc)" +RUNTIME_GID="$(id -g abc)" + +for managed_path in "$HOME/.claude" "$HOME/.claude.json" "$HOME/.config/Claude"; do + if [ -e "$managed_path" ]; then + chown -R -- "${RUNTIME_UID}:${RUNTIME_GID}" "$managed_path" \ + || bashio::log.warning "Unable to set effective runtime ownership on $managed_path" + fi +done diff --git a/claude_desktop/rootfs/usr/local/bin/claude b/claude_desktop/rootfs/usr/local/bin/claude index ac0a27a966..ec56d152be 100644 --- a/claude_desktop/rootfs/usr/local/bin/claude +++ b/claude_desktop/rootfs/usr/local/bin/claude @@ -27,6 +27,17 @@ if [ ! -x "$REAL_CLAUDE" ]; then exit 127 fi +# Claude Code rejects bypass mode when the effective UID is 0. Normal Desktop sessions run +# as abc, which startup remaps to a non-root UID when bypass is selected. Also handle a user +# invoking this wrapper directly from a root container console by dropping to abc here. +if [ "$PERMISSION_MODE" = "bypass" ] && [ "$(id -u)" -eq 0 ]; then + if command -v s6-setuidgid > /dev/null 2>&1 && [ "$(id -u abc)" -ne 0 ]; then + exec s6-setuidgid abc "$0" "$@" + fi + echo "claude wrapper: bypass mode requires a non-root runtime user, but abc is still UID 0" >&2 + exit 1 +fi + if bashio::config.true 'install_headroom' && bashio::config.true 'headroom_wrap_claude_code'; then if [ -x "$HEADROOM_BIN" ] && curl -fsS --max-time 2 "${HEADROOM_URL}/health" > /dev/null 2>&1; then # Put /usr/bin before /usr/local/bin while Headroom resolves its upstream `claude` 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 9ae7dda7da..150b73f46d 100755 --- a/claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh +++ b/claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh @@ -26,6 +26,18 @@ for option in permission_mode install_headroom headroom_wrap_claude_code expose_ printf '%-30s %s\n' "$option" "$(bashio::config "$option")" done +section "Runtime identity" +printf '%-30s %s\n' "configured PUID:PGID" "$(bashio::config 'PUID'):$(bashio::config 'PGID')" +printf '%-30s %s\n' "effective abc UID:GID" "$(id -u abc):$(id -g abc)" +printf '%-30s %s\n' "current process UID:GID" "$(id -u):$(id -g)" +if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then + if [ "$(id -u abc)" -eq 0 ]; then + echo "bypass runtime: ERROR - Claude Code will reject bypass permissions while abc is root" + else + echo "bypass runtime: OK - Claude Desktop and Cowork run as a non-root UID" + fi +fi + section "Claude Code permission state" python3 - <<'PY' import json