Merge pull request #2862 from alexbelgium/agent/claude-bypass-nonroot

Fix Claude bypass permissions under root PUID
This commit is contained in:
Alexandre
2026-07-15 11:17:27 +02:00
committed by GitHub
8 changed files with 128 additions and 11 deletions

View File

@@ -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) ## 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`. - 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`.

View File

@@ -35,6 +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
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 - **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.
@@ -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. - Persistent sign-in through a bundled, auto-unlocked gnome-keyring.
- Configurable Claude Code permissions: strict prompts, automatic safe-action - Configurable Claude Code permissions: strict prompts, automatic safe-action
approval, or explicit full bypass for trusted installations. 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 runtime Claude Desktop updates from Anthropic's apt repository.
- Optional extra apt and pip package installation (pip installs use `uv`). - Optional extra apt and pip package installation (pip installs use `uv`).
- Baked-in `git`, GitHub CLI (`gh`), `ripgrep`, `jq`, `shellcheck`, `yamllint`, - 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. Assistant.
- Independent hourly savings reports for Headroom, RTK, and TokenSave. - Independent hourly savings reports for Headroom, RTK, and TokenSave.
- `claude-tools-doctor.sh` diagnostics for binaries, routing, hooks, MCP - `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. - Low-power defaults for GPU mapping, Selkies frame rate, and volatile caches.
## Options ## Options
| Option | Default | Description | | 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`. | | `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. |
@@ -122,9 +129,21 @@ permission_mode: auto
`bypassPermissions` in the shared settings and `bypassPermissions` in the shared settings and
`--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
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 `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 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 ### 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 claude-tools-doctor.sh
``` ```
The report checks the tool binaries, configuration switches, redacted MCP The report checks the tool binaries, configuration switches, configured and
registrations, Claude hooks, permission mode, Headroom health, TokenSave indexes, effective runtime identities, redacted MCP registrations, Claude hooks,
routing, and recorded savings. It never prints MCP environment values because permission mode, Headroom health, TokenSave indexes, routing, and recorded
the Home Assistant MCP entry can contain a long-lived token. 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: The hourly report can also be invoked manually:

View File

@@ -103,5 +103,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.20" version: "1.21"
video: true video: true

View File

@@ -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))"

View File

@@ -3,9 +3,10 @@
# shellcheck disable=SC2046 # shellcheck disable=SC2046
set -e set -e
# Define user # Use the effective shared desktop user identity. In bypass mode an earlier init script may
PUID=$(bashio::config "PUID") # remap abc away from UID 0 because Claude Code rejects bypass permissions when run as root.
PGID=$(bashio::config "PGID") PUID="$(id -u abc)"
PGID="$(id -g abc)"
# Check data location # Check data location
LOCATION="$(bashio::config 'data_location')" LOCATION="$(bashio::config 'data_location')"

View File

@@ -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

View File

@@ -27,6 +27,17 @@ if [ ! -x "$REAL_CLAUDE" ]; then
exit 127 exit 127
fi 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 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 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` # Put /usr/bin before /usr/local/bin while Headroom resolves its upstream `claude`

View File

@@ -26,6 +26,18 @@ for option in permission_mode install_headroom headroom_wrap_claude_code expose_
printf '%-30s %s\n' "$option" "$(bashio::config "$option")" printf '%-30s %s\n' "$option" "$(bashio::config "$option")"
done 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" section "Claude Code permission state"
python3 - <<'PY' python3 - <<'PY'
import json import json