Files
hassio-addons/claude_desktop/rootfs/etc/cont-init.d/20-folders.sh
alexbelgium bdc6231aa3 fix(claude_desktop): repair Headroom MCP model cache, HOME dedup, gitconfig owner
Three add-on runtime-environment bugs, all found while investigating a Headroom
dashboard stuck at 0 gain.

Headroom MCP server had no HF_HOME. 1.27 fixed the Kompress model cache for the
svc-headroom proxy longrun by exporting HF_HOME there, but the MCP server is a
different process: Claude Desktop and Claude Code spawn it from the registered
mcpServers entry, so it never saw that export and kept resolving the HuggingFace
cache to ~/.cache, which this add-on symlinks to tmpfs. Its Kompress ML path
therefore never found the model, re-downloaded ~270 MB into tmpfs on every boot,
and lost it on the next one -- headroom_compress returned router:noop (output
unchanged) for prose and other unstructured content. Rule-based compression
(SmartCrusher, structured tool output) was unaffected and worked throughout,
which is why the failure only showed on some payloads. Carry env.HF_HOME on the
managed headroom entry in both claude_desktop_config.json and ~/.claude.json.

~/.gitconfig was written as root and left unreadable by abc. `git config --global`
ran as root during init and rewrites the file on every start, so 20-folders.sh's
earlier recursive chown never stuck to it; .config/gh survived abc-owned only
because the "already authenticated" branch skips rewriting it. The user that
actually runs git, gh and Claude could not read its own committer identity or the
gh credential helper: every commit failed with "Author identity unknown" and
authenticated pushes fell back to prompting. Run the git/gh setup as abc via
s6-setuidgid, matching 81-tokensave_repositories.sh, and reclaim root-owned
copies left by earlier versions before writing.

~/.bashrc accumulated stale HOME/FM_HOME exports across data_location changes.
The idempotency guard only tested for the current $LOCATION, so changing the
option and later changing it back appended a second block while leaving the first,
and the last one written won for every interactive shell. $HOME then pointed at a
directory the add-on no longer manages, so anything resolving config through it
read the wrong path -- `headroom doctor` reported "claude: not routed (no
~/.claude/settings.json)" against a correctly routed install, and bare `headroom`
invocations created a stray .headroom tree under the old location. Make the block
marker-delimited and rewrite it from scratch each boot.

Verified on a running add-on: headroom_compress now reports 1909 -> 1122 tokens
(41.2%, router:mixed) through the live MCP server; `headroom doctor` reports
"claude: routed via /data/data/.claude/settings.json"; and git commits work as abc
without a repo-local identity override.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:36:05 +02:00

152 lines
6.7 KiB
Bash
Executable File

#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
# shellcheck disable=SC2046
set -e
# 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')"
if [[ "$LOCATION" = "null" || -z "$LOCATION" ]]; then
LOCATION="/data/data"
else
LOCATIONOK=""
for location in "/share" "/config" "/data" "/mnt"; do
if [[ "$LOCATION" == "$location"* ]]; then
LOCATIONOK=true
fi
done
if [ -z "$LOCATIONOK" ]; then
LOCATION="/data/data"
bashio::log.fatal "Your data_location value can only be set in /share, /config, /data or /mnt. It will be reset to the default location : $LOCATION"
fi
fi
bashio::log.info "Setting data location to $LOCATION"
# LinuxServer Selkies services expect the s6 envdir at /run/s6/container_environment.
# Home Assistant add-on startup can run without s6-overlay PID1, so create the envdir
# ourselves before those services start and before scripts write required variables.
S6_ENVDIR="/run/s6/container_environment"
mkdir -p "$S6_ENVDIR"
chmod 755 /run/s6 "$S6_ENVDIR"
XDG_RUNTIME_DIR="/run/user/$PUID"
mkdir -p "$XDG_RUNTIME_DIR"
chmod 700 "$XDG_RUNTIME_DIR"
for file in /etc/s6-overlay/s6-rc.d/*/run; do
if [ "$(sed -n '1{/bash/p};q' "$file")" ] && ! grep -q '^export XDG_CACHE_HOME=/tmp/cache$' "$file"; then
sed -i "1a export HOME=$LOCATION" "$file"
sed -i "1a export FM_HOME=$LOCATION" "$file"
sed -i "1a export XDG_CACHE_HOME=/tmp/cache" "$file"
fi
done
for folders in /defaults /etc/cont-init.d /etc/services.d /etc/s6-overlay/s6-rc.d; do
if [ -d "$folders" ]; then
find "$folders" -type f -exec sed -i "s|/data/data|$LOCATION|g" {} + &> /dev/null || true
fi
done
sed -i "s|^\(abc:[^:]*:[^:]*:[^:]*:[^:]*:\)[^:]*|\1$LOCATION|" /etc/passwd
printf "%s" "$LOCATION" > "$S6_ENVDIR/HOME"
printf "%s" "$LOCATION" > "$S6_ENVDIR/FM_HOME"
printf "%s" "/tmp/cache" > "$S6_ENVDIR/XDG_CACHE_HOME"
printf "%s" "$XDG_RUNTIME_DIR" > "$S6_ENVDIR/XDG_RUNTIME_DIR"
# Re-derived on every boot rather than gated on a "does it already say $LOCATION" grep: that
# guard only ever recognized the CURRENT $LOCATION, so a user who changed data_location and
# later changed it back left two stale HOME/FM_HOME exports in ~/.bashrc, with the last one
# (not necessarily the correct one) winning for every interactive shell. The marker makes this
# idempotent regardless of how many times $LOCATION has changed: strip any previously managed
# block, then append one that reflects the current value.
BASHRC_HOME_BEGIN="# --- BEGIN ADDON HOME (managed) ---"
BASHRC_HOME_END="# --- END ADDON HOME (managed) ---"
if [ -f ~/.bashrc ]; then
sed -i "/^${BASHRC_HOME_BEGIN}\$/,/^${BASHRC_HOME_END}\$/d" ~/.bashrc
fi
{
printf "%s\n" "$BASHRC_HOME_BEGIN"
printf "%s\n" "export HOME=\"$LOCATION\""
printf "%s\n" "export FM_HOME=\"$LOCATION\""
printf "%s\n" "export XDG_CACHE_HOME=\"/tmp/cache\""
printf "%s\n" "$BASHRC_HOME_END"
} >> ~/.bashrc
bashio::log.info "Creating $LOCATION"
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
chmod 777 /tmp/selkies_js.log
if [ -e "$LOCATION/.cache" ] && [ ! -L "$LOCATION/.cache" ]; then
rm -rf "$LOCATION/.cache"
fi
ln -sfn /tmp/cache "$LOCATION/.cache"
bashio::log.info "Setting ownership to $PUID:$PGID"
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
# on persistent storage and conflicts with the tmpfs runtime dir set above. Re-assert the
# tmpfs value at the end of that oneshot so the app and desktop agree on one valid dir.
SELKIES_CONFIG_RUN="/etc/s6-overlay/s6-rc.d/init-selkies-config/run"
if [ -f "$SELKIES_CONFIG_RUN" ]; then
sed -i '/^# XDG_RUNTIME_DIR override reconciled$/,+1d' "$SELKIES_CONFIG_RUN"
printf '\n# XDG_RUNTIME_DIR override reconciled\nprintf "%%s" "%s" > /run/s6/container_environment/XDG_RUNTIME_DIR\n' "$XDG_RUNTIME_DIR" >> "$SELKIES_CONFIG_RUN"
fi
# The Selkies desktop init oneshots do best-effort device/permission setup (mknod
# /dev/input/*, chmod /tmp/selkies*, /dev/dri perms) that is only partially permitted in the
# HA add-on sandbox. A non-zero exit from a oneshot fails add-on bringup and crash-loops the
# container, so make these two tolerant and always report success. Longruns (svc-*) are left
# untouched so s6 keeps supervising them with their real exit codes.
for oneshot in init-video init-selkies-config; do
run="/etc/s6-overlay/s6-rc.d/$oneshot/run"
if [ -f "$run" ] && ! grep -q '^set +e$' "$run"; then
sed -i "1a set +e" "$run"
printf '\nexit 0\n' >> "$run"
fi
done