From c04f3e288ccef2d2299ac0dc76ad8e86b849bcd2 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:11:20 +0200 Subject: [PATCH 01/18] Improve Claude optimization tooling --- claude_desktop/config.yaml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/claude_desktop/config.yaml b/claude_desktop/config.yaml index 7987560804..d6b4631ec5 100644 --- a/claude_desktop/config.yaml +++ b/claude_desktop/config.yaml @@ -2,7 +2,7 @@ arch: - aarch64 - amd64 audio: true -description: "Claude Desktop with Headroom MCP context compression and RTK acceleration" +description: "Claude Desktop with Headroom, RTK, and TokenSave optimization" devices: - /dev/dri - /dev/dri/card0 @@ -46,19 +46,23 @@ options: ha_mcp_token: "" github_token: "" github_username: "" - install_caveman: true + enable_tools_health_report: true + expose_headroom_dashboard: false + headroom_wrap_claude_code: true + install_caveman: false install_github_cli: true install_headroom: true install_rtk: true install_tokensave: true + tokensave_project_paths: [] panel_admin: false panel_icon: mdi:robot-happy ports: 3001/tcp: null - 8787/tcp: 8787 + 8787/tcp: null ports_description: 3001/tcp: Claude Desktop web interface - 8787/tcp: Headroom dashboard and proxy + 8787/tcp: Optional Headroom dashboard and proxy privileged: - SYS_ADMIN - DAC_READ_SEARCH @@ -83,14 +87,19 @@ schema: ha_mcp_token: password? github_token: password? github_username: str? + enable_tools_health_report: bool + expose_headroom_dashboard: bool + headroom_wrap_claude_code: bool install_caveman: bool install_github_cli: bool install_headroom: bool install_rtk: bool install_tokensave: bool + tokensave_project_paths: + - str slug: claude_desktop tmpfs: true udev: true url: https://github.com/alexbelgium/hassio-addons -version: "1.19" +version: "1.20" video: true From c8d706c9ded874e550115096ab460fdd9b1761ec Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:11:30 +0200 Subject: [PATCH 02/18] Improve Claude optimization tooling --- .../rootfs/etc/s6-overlay/s6-rc.d/svc-headroom/run | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-headroom/run b/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-headroom/run index 475c5a47cc..a1ee38d6b8 100755 --- a/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-headroom/run +++ b/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-headroom/run @@ -1,11 +1,15 @@ #!/usr/bin/with-contenv bashio # Headroom optimization proxy — local backend for Claude Desktop MCP and Claude Code. declare port=8787 -# Bind all interfaces so the dashboard is reachable on the mapped host port -# (http://:8787/dashboard). Local consumers keep using 127.0.0.1. -declare host=0.0.0.0 +declare host=127.0.0.1 -if bashio::config.true 'install_headroom' && command -v headroom >/dev/null 2>&1; then +# The dashboard is unauthenticated. Keep it container-local by default and bind all +# interfaces only when the user explicitly opts in and maps port 8787. +if bashio::config.true 'expose_headroom_dashboard'; then + host=0.0.0.0 +fi + +if bashio::config.true 'install_headroom' && command -v headroom > /dev/null 2>&1; then bashio::log.info "svc-headroom: starting local Headroom proxy on ${host}:${port}" exec s6-setuidgid abc headroom proxy --host "${host}" --port "${port}" --code-aware fi From 3c8a32199ed89a13de33e5e8337c4cbef2e280ff Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:11:46 +0200 Subject: [PATCH 03/18] Improve Claude optimization tooling --- .../usr/local/bin/claude-gains-report.sh | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/claude_desktop/rootfs/usr/local/bin/claude-gains-report.sh b/claude_desktop/rootfs/usr/local/bin/claude-gains-report.sh index ad8580a3e8..6a9bfebcb1 100755 --- a/claude_desktop/rootfs/usr/local/bin/claude-gains-report.sh +++ b/claude_desktop/rootfs/usr/local/bin/claude-gains-report.sh @@ -1,34 +1,44 @@ #!/usr/bin/with-contenv bashio -# Hourly rtk + headroom token-savings snapshot for the add-on log. -# Invoked by cron (see /defaults/crontabs/root); its stdout is redirected to /proc/1/fd/1, -# so the report appears in the add-on log. Doubles as a heartbeat: if the numbers stop -# growing, the corresponding tool has stopped working. -# with-contenv supplies HOME from the s6 envdir, so this honors a custom `data_location` -# (see 20-folders.sh) instead of hardcoding /data/data; it also makes bashio::config -# available for the install_headroom gate below. -export NO_COLOR=1 # keep the add-on log free of ANSI color codes +# Hourly RTK + Headroom + TokenSave token-savings snapshot for the add-on log. +# Invoked by cron (see /defaults/crontabs/root); stdout is redirected to /proc/1/fd/1. +# Each tool is reported independently so enabling Headroom cannot hide RTK or TokenSave data. +# with-contenv supplies the configured persistent HOME. +export NO_COLOR=1 export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}" -have_rtk=false; command -v rtk >/dev/null 2>&1 && have_rtk=true -have_headroom=false; command -v headroom >/dev/null 2>&1 && have_headroom=true - -# headroom is pip-installed unconditionally at build time, so its binary is on PATH even -# when install_headroom is off — gate on the same config svc-headroom checks, and only -# fall back to have_headroom as a secondary availability guard. -headroom_enabled=false -if bashio::config.true 'install_headroom' && $have_headroom; then - headroom_enabled=true +if ! bashio::config.true 'enable_tools_health_report'; then + exit 0 fi -# Nothing to report if neither tool is active — stay quiet. -if ! $have_rtk && ! $headroom_enabled; then exit 0; fi +rtk_enabled=false +headroom_enabled=false +tokensave_enabled=false -echo "===== claude gains report $(date '+%Y-%m-%d %H:%M:%S') =====" +if bashio::config.true 'install_rtk' && command -v rtk > /dev/null 2>&1; then + rtk_enabled=true +fi +if bashio::config.true 'install_headroom' && command -v headroom > /dev/null 2>&1; then + headroom_enabled=true +fi +if bashio::config.true 'install_tokensave' && command -v tokensave > /dev/null 2>&1; then + tokensave_enabled=true +fi + +if ! $rtk_enabled && ! $headroom_enabled && ! $tokensave_enabled; then + exit 0 +fi + +echo "===== claude tools report $(date '+%Y-%m-%d %H:%M:%S') =====" if $headroom_enabled; then echo "--- headroom savings ---" headroom savings 2>&1 || echo "[warn] headroom savings failed" -elif $have_rtk; then +fi +if $rtk_enabled; then echo "--- rtk gain ---" rtk gain 2>&1 || echo "[warn] rtk gain failed" fi -echo "===== end gains report =====" +if $tokensave_enabled; then + echo "--- tokensave gain ---" + tokensave gain --all --range 30d 2>&1 || echo "[warn] tokensave gain failed" +fi +echo "===== end claude tools report =====" From 0ff2e8f78307378317688e282360bda653abdd5d Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:11:58 +0200 Subject: [PATCH 04/18] Improve Claude optimization tooling --- claude_desktop/rootfs/defaults/crontabs/root | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/claude_desktop/rootfs/defaults/crontabs/root b/claude_desktop/rootfs/defaults/crontabs/root index fbfdd3f0c5..7a86e67d86 100644 --- a/claude_desktop/rootfs/defaults/crontabs/root +++ b/claude_desktop/rootfs/defaults/crontabs/root @@ -1,4 +1,4 @@ -# Hourly rtk + headroom token-savings report to the add-on log (heartbeat + gains). +# Hourly RTK + Headroom + TokenSave savings report to the add-on log. # Seeded to /data/data/crontabs/root by init-crontab-config and run by svc-cron; edit the # persistent copy to customize. Output goes to /proc/1/fd/1 so it shows in the add-on log. 0 * * * * /usr/local/bin/claude-gains-report.sh > /proc/1/fd/1 2>&1 From 006f3052accb841ab5141561077b0aec261f5f96 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:12:14 +0200 Subject: [PATCH 05/18] Improve Claude optimization tooling --- claude_desktop/rootfs/usr/local/bin/claude | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 claude_desktop/rootfs/usr/local/bin/claude diff --git a/claude_desktop/rootfs/usr/local/bin/claude b/claude_desktop/rootfs/usr/local/bin/claude new file mode 100644 index 0000000000..e4864e1e1e --- /dev/null +++ b/claude_desktop/rootfs/usr/local/bin/claude @@ -0,0 +1,25 @@ +#!/usr/bin/with-contenv bashio +# shellcheck shell=bash +set -o pipefail + +REAL_CLAUDE="/usr/bin/claude" +HEADROOM_BIN="/usr/local/bin/headroom" +HEADROOM_URL="http://127.0.0.1:8787" + +if [ ! -x "$REAL_CLAUDE" ]; then + echo "claude wrapper: ${REAL_CLAUDE} is unavailable" >&2 + exit 127 +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` + # executable; otherwise it would resolve this wrapper recursively. + export HEADROOM_CONTEXT_TOOL="rtk" + exec env PATH="/usr/bin:/bin:/usr/local/bin" \ + "$HEADROOM_BIN" wrap claude --no-proxy -- "$@" + fi + echo "claude wrapper: Headroom proxy is unavailable; launching Claude Code directly" >&2 +fi + +exec "$REAL_CLAUDE" "$@" From 22951ac4f564ae7444181ef3627f154948a1e645 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:12:44 +0200 Subject: [PATCH 06/18] Improve Claude optimization tooling --- .../usr/local/bin/claude-tools-doctor.sh | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh diff --git a/claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh b/claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh new file mode 100644 index 0000000000..0f16921f05 --- /dev/null +++ b/claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh @@ -0,0 +1,141 @@ +#!/usr/bin/with-contenv bashio +# Diagnose installation, registration, routing, indexing, and recorded savings without +# printing MCP environment values (which may contain the Home Assistant access token). +# shellcheck shell=bash +set +e +set -o pipefail +export NO_COLOR=1 +export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}" + +section() { + printf '\n=== %s ===\n' "$1" +} + +section "Installed binaries" +for tool in claude claude-desktop headroom rtk tokensave git gh rg jq shellcheck yamllint hadolint actionlint; do + resolved="$(command -v "$tool" 2> /dev/null || true)" + if [ -n "$resolved" ]; then + printf '%-16s %s\n' "$tool" "$resolved" + else + printf '%-16s %s\n' "$tool" "MISSING" + fi +done + +section "Configured switches" +for option in install_headroom headroom_wrap_claude_code expose_headroom_dashboard install_rtk install_tokensave install_caveman enable_tools_health_report; do + printf '%-30s %s\n' "$option" "$(bashio::config "$option")" +done + +section "MCP registrations (environment values redacted)" +python3 - <<'PY' +import json +from pathlib import Path + +paths = [ + Path.home() / ".claude.json", + Path.home() / ".config/Claude/claude_desktop_config.json", +] +for path in paths: + print(path) + try: + data = json.loads(path.read_text()) + except FileNotFoundError: + print(" MISSING") + continue + except Exception as exc: + print(f" INVALID: {exc}") + continue + servers = data.get("mcpServers", {}) + if not isinstance(servers, dict) or not servers: + print(" no MCP servers") + continue + for name, spec in sorted(servers.items()): + if not isinstance(spec, dict): + print(f" {name}: invalid entry") + continue + command = spec.get("command", "?") + args = spec.get("args", []) + server_type = spec.get("type", "") + suffix = f" type={server_type}" if server_type else "" + print(f" {name}: {command} {args}{suffix}") + if spec.get("env"): + print(" env: ") +PY + +section "Claude Code hooks" +python3 - <<'PY' +import json +from pathlib import Path + +path = Path.home() / ".claude/settings.json" +try: + data = json.loads(path.read_text()) +except FileNotFoundError: + print("MISSING") + raise SystemExit(0) +except Exception as exc: + print(f"INVALID: {exc}") + raise SystemExit(0) + +hooks = data.get("hooks", {}) +if not isinstance(hooks, dict) or not hooks: + print("no hooks") + raise SystemExit(0) +for event, entries in hooks.items(): + print(event) + if not isinstance(entries, list): + print(" invalid entries") + continue + for entry in entries: + matcher = entry.get("matcher", "*") if isinstance(entry, dict) else "?" + commands = entry.get("hooks", []) if isinstance(entry, dict) else [] + rendered = [] + for command in commands if isinstance(commands, list) else []: + if isinstance(command, dict): + rendered.append(" ".join([str(command.get("command", "?")), *map(str, command.get("args", []))])) + print(f" matcher={matcher}: {', '.join(rendered) or 'no command'}") +PY + +section "Headroom" +if bashio::config.true 'install_headroom'; then + curl -fsS --max-time 2 http://127.0.0.1:8787/health && echo || echo "proxy health: FAILED" + headroom mcp status || true + headroom savings || true +else + echo "disabled" +fi + +section "RTK" +if bashio::config.true 'install_rtk'; then + rtk gain || true +else + echo "disabled" +fi + +section "TokenSave" +if bashio::config.true 'install_tokensave'; then + tokensave doctor --agent claude || true + tokensave gain --all --range 30d || true + while IFS= read -r configured_path; do + [ -n "$configured_path" ] || continue + repo_root="$(git -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)" + if [ -z "$repo_root" ]; then + echo "${configured_path}: not a Git repository" + elif [ -f "$repo_root/.tokensave/tokensave.db" ]; then + tokensave status "$repo_root" --short || true + else + echo "${repo_root}: NOT INITIALIZED" + fi + done < <(bashio::config.array 'tokensave_project_paths') +else + echo "disabled" +fi + +section "Claude routing" +printf 'PATH claude: %s\n' "$(command -v claude 2> /dev/null || true)" +printf 'real claude: %s\n' "$([ -x /usr/bin/claude ] && echo /usr/bin/claude || echo MISSING)" +if bashio::config.true 'headroom_wrap_claude_code'; then + echo "PATH-based Claude Code launches are configured for Headroom wrapping." +else + echo "Claude Code Headroom wrapping is disabled; Headroom remains available through MCP." +fi From 6b6233e1fa9b9bd171adf4a5cc441be79da33038 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:14:23 +0200 Subject: [PATCH 07/18] Improve Claude optimization tooling --- .../rootfs/etc/cont-init.d/82-claude_tools.sh | 97 +++++++++++++++---- 1 file changed, 78 insertions(+), 19 deletions(-) diff --git a/claude_desktop/rootfs/etc/cont-init.d/82-claude_tools.sh b/claude_desktop/rootfs/etc/cont-init.d/82-claude_tools.sh index 17f6a2084a..cd8e9fbf9e 100755 --- a/claude_desktop/rootfs/etc/cont-init.d/82-claude_tools.sh +++ b/claude_desktop/rootfs/etc/cont-init.d/82-claude_tools.sh @@ -7,15 +7,18 @@ PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else ech PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '0'; fi)" mkdir -p "$HOME/.claude" +run_as_runtime_user() { + s6-setuidgid abc env HOME="$HOME" "$@" +} + CLAUDE_DESKTOP_COMMAND_FILE="/tmp/claude-desktop-command" DEFAULT_CLAUDE_DESKTOP_COMMAND='claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret' printf '%s\n' "$DEFAULT_CLAUDE_DESKTOP_COMMAND" > "$CLAUDE_DESKTOP_COMMAND_FILE" -# headroom's "wrap"/proxy routing works by setting ANTHROPIC_BASE_URL, which the Claude Desktop -# Electron app force-overrides to the production endpoint (headroom #869), so transparent -# compression cannot be applied to the desktop launch. The integration that does work with -# Claude Desktop is headroom's MCP server, which exposes the headroom_compress/headroom_retrieve/ -# headroom_stats tools inside the app. +# Headroom's proxy routing works by setting ANTHROPIC_BASE_URL, which the Claude Desktop +# Electron app force-overrides to the production endpoint (headroom #869). Desktop therefore +# uses Headroom's MCP tools. Claude Code launches that resolve `claude` through PATH use the +# add-on's /usr/local/bin/claude wrapper and can be transparently proxied when enabled. # # Register the add-on-managed MCP servers (headroom, tokensave, homeassistant) in both Claude # Desktop's config and Claude Code's user config (used by Desktop cowork/dispatch sessions). @@ -38,10 +41,18 @@ TOKENSAVE_ENABLED=false if bashio::config.true 'install_tokensave'; then if command -v tokensave &> /dev/null; then TOKENSAVE_ENABLED=true - bashio::log.info "tokensave $(tokensave --version 2> /dev/null || true) available; registering the tokensave MCP server" + bashio::log.info "tokensave $(tokensave --version 2> /dev/null || true) available; configuring the complete Claude Code integration" + # The upstream installer adds the MCP entry, PreToolUse/UserPromptSubmit/Stop hooks, + # MCP permissions, global CLAUDE.md rules, and the global post-commit/checkout sync hook. + run_as_runtime_user tokensave install --agent claude --git-hook yes \ + || bashio::log.warning "tokensave Claude Code integration setup failed" else bashio::log.warning "tokensave is not available" fi +elif command -v tokensave &> /dev/null; then + bashio::log.info "Removing the tokensave Claude Code integration" + run_as_runtime_user tokensave uninstall --agent claude \ + || bashio::log.warning "tokensave Claude Code integration removal failed" fi HA_MCP_ENABLED=false @@ -80,7 +91,10 @@ MANAGED_BASENAMES = { desired = {} if os.environ["HEADROOM_ENABLED"] == "true": - desired["headroom"] = {"command": os.environ["HEADROOM_BIN"], "args": ["mcp", "serve"]} + desired["headroom"] = { + "command": os.environ["HEADROOM_BIN"], + "args": ["mcp", "serve", "--proxy-url", "http://127.0.0.1:8787"], + } if os.environ["TOKENSAVE_ENABLED"] == "true": desired["tokensave"] = {"command": os.environ["TOKENSAVE_BIN"], "args": ["serve"]} if os.environ["HA_MCP_ENABLED"] == "true": @@ -98,6 +112,7 @@ if os.environ["HA_MCP_ENABLED"] == "true": # under $HOME stay untouched because those are user-installed. HOME_PREFIX = os.path.expanduser("~") + os.sep + def is_managed(name, entry): if not isinstance(entry, dict): return False @@ -106,6 +121,7 @@ def is_managed(name, entry): return False return os.path.basename(command) == MANAGED_BASENAMES[name] + for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_CONFIG", True)): path = Path(os.environ[config_var]) try: @@ -145,12 +161,56 @@ for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_C path.chmod(0o600) PY -# Guide Claude to actually use the headroom compression tools so the MCP integration produces -# real savings (otherwise the tools sit unused and `headroom savings` stays empty). Managed, -# idempotent block appended to the user's global CLAUDE.md; removed when headroom is disabled. +# Initialize or incrementally sync only explicitly configured repositories. TokenSave deliberately +# requires one-time per-project opt-in; an empty list therefore has no startup or storage cost. +if $TOKENSAVE_ENABLED; then + declare -A TOKENSAVE_REPOS_SEEN=() + while IFS= read -r configured_path; do + # Trim surrounding whitespace while preserving spaces inside paths. + configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}" + configured_path="${configured_path%"${configured_path##*[![:space:]]}"}" + [ -n "$configured_path" ] || continue + + case "$configured_path" in + /*) ;; + *) + bashio::log.warning "Skipping non-absolute tokensave_project_paths entry: ${configured_path}" + continue + ;; + esac + if [ ! -d "$configured_path" ]; then + bashio::log.warning "Skipping missing TokenSave project path: ${configured_path}" + continue + fi + + repo_root="$(git -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)" + if [ -z "$repo_root" ] || [ "$repo_root" = "/" ]; then + bashio::log.warning "Skipping TokenSave path that is not a supported Git repository: ${configured_path}" + continue + fi + if [[ -n "${TOKENSAVE_REPOS_SEEN[$repo_root]:-}" ]]; then + continue + fi + TOKENSAVE_REPOS_SEEN[$repo_root]=1 + + if [ -f "$repo_root/.tokensave/tokensave.db" ]; then + bashio::log.info "Synchronizing TokenSave index: ${repo_root}" + run_as_runtime_user tokensave sync "$repo_root" \ + || bashio::log.warning "TokenSave sync failed for ${repo_root}" + else + bashio::log.info "Initializing TokenSave index: ${repo_root}" + run_as_runtime_user tokensave init "$repo_root" \ + || bashio::log.warning "TokenSave initialization failed for ${repo_root}" + fi + done < <(bashio::config.array 'tokensave_project_paths') +fi + +# Guide Claude to actually use the Headroom compression tools so the MCP integration produces +# real savings when transparent proxying is unavailable. Managed, idempotent block appended to +# the user's global CLAUDE.md; removed when Headroom is disabled. CLAUDE_MD="$HOME/.claude/CLAUDE.md" HEADROOM_GUIDE_BEGIN="" -if bashio::config.true 'install_headroom'; then +if $HEADROOM_ENABLED; then mkdir -p "$(dirname "$CLAUDE_MD")" if ! { [ -f "$CLAUDE_MD" ] && grep -qF "$HEADROOM_GUIDE_BEGIN" "$CLAUDE_MD"; }; then bashio::log.info "Adding headroom usage guidance to CLAUDE.md" @@ -193,14 +253,13 @@ fi if bashio::config.true 'install_rtk'; then if command -v rtk &> /dev/null; then - if [ -f "$HOME/.claude/settings.json" ] && grep -q 'rtk hook claude' "$HOME/.claude/settings.json"; then - bashio::log.info "rtk Claude Code hook already configured" - else - bashio::log.info "Configuring rtk Claude Code hook" - RTK_NONINTERACTIVE=1 rtk init -g || bashio::log.warning "rtk global files configuration failed" - python3 - <<'PY' || bashio::log.warning "Unable to configure rtk hook automatically" + bashio::log.info "Configuring rtk Claude Code integration" + run_as_runtime_user env RTK_NONINTERACTIVE=1 rtk init -g \ + || bashio::log.warning "rtk global files configuration failed" + python3 - <<'PY' || bashio::log.warning "Unable to configure rtk hook automatically" import json from pathlib import Path + path = Path.home() / ".claude" / "settings.json" try: data = json.loads(path.read_text()) if path.exists() else {} @@ -218,7 +277,6 @@ if not any("rtk hook claude" in json.dumps(entry) for entry in pre if isinstance path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, indent=2) + "\n") PY - fi else bashio::log.warning "rtk is not available" fi @@ -290,7 +348,8 @@ if bashio::config.true 'install_caveman'; then bashio::log.info "caveman Claude Code plugin already configured" else bashio::log.info "Installing caveman Claude Code plugin" - curl --connect-timeout 10 --max-time 60 -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash >/dev/null || bashio::log.warning "caveman install failed (offline?)" + curl --connect-timeout 10 --max-time 60 -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash > /dev/null \ + || bashio::log.warning "caveman install failed (offline?)" fi else bashio::log.info "Disabling caveman Claude Code plugin" From 9c55193c38314272f31ddce0b9388bab6a2a8601 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:15:18 +0200 Subject: [PATCH 08/18] Improve Claude optimization tooling --- claude_desktop/Dockerfile | 46 ++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/claude_desktop/Dockerfile b/claude_desktop/Dockerfile index 1ef8f27484..ccf216a9ed 100644 --- a/claude_desktop/Dockerfile +++ b/claude_desktop/Dockerfile @@ -34,6 +34,7 @@ RUN cargo install tokensave --version "${TOKENSAVE_VERSION}" --locked --root /ou /out/bin/tokensave --version FROM ${BUILD_FROM} +ARG BUILD_ARCH ################## # 2 Modify Image # @@ -73,15 +74,17 @@ RUN curl -fsSL --retry 3 --retry-delay 2 \ # cannot alter executables elsewhere in the image. COPY rootfs/ / RUN find /etc/cont-init.d /etc/s6-overlay /defaults /usr/local/bin -type f \ - \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \; + \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \; && \ + chmod +x /usr/local/bin/claude # Uses /bin for compatibility purposes # hadolint ignore=DL4005 RUN if [ ! -f /bin/sh ] && [ -f /usr/bin/sh ]; then ln -s /usr/bin/sh /bin/sh; fi && \ if [ ! -f /bin/bash ] && [ -f /usr/bin/bash ]; then ln -s /usr/bin/bash /bin/bash; fi -# Install Claude Desktop, Claude Code, and Python tooling. gnome-keyring provides the -# Secret Service backend Electron safeStorage needs to persist sign-in and dispatch grants. +# Install Claude Desktop, Claude Code, Python tooling, and lightweight local validators. +# gnome-keyring provides the Secret Service backend Electron safeStorage needs to persist +# sign-in and dispatch grants. RUN install -d -m 0755 /etc/apt/keyrings && \ curl -fsSLo /usr/share/keyrings/claude-desktop-archive-keyring.asc https://downloads.claude.ai/claude-desktop/key.asc && \ curl -fsSLo /etc/apt/keyrings/claude-code.asc https://downloads.claude.ai/keys/claude-code.asc && \ @@ -96,10 +99,42 @@ RUN install -d -m 0755 /etc/apt/keyrings && \ dbus-x11 \ git \ gh \ - ripgrep && \ + ripgrep \ + jq \ + shellcheck \ + yamllint && \ + test -x /usr/bin/claude && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* +# Install the current upstream hadolint and actionlint releases for both supported +# architectures. The GitHub release API resolves the latest asset at build time, so these +# developer tools are intentionally not version-pinned. +RUN set -eux; \ + case "${BUILD_ARCH}" in \ + amd64) hadolint_arch="x86_64"; actionlint_arch="x86_64" ;; \ + aarch64) hadolint_arch="arm64"; actionlint_arch="arm64" ;; \ + *) echo "Unsupported validation-tools architecture: ${BUILD_ARCH}" >&2; exit 1 ;; \ + esac; \ + hadolint_name="hadolint-Linux-${hadolint_arch}"; \ + hadolint_url="$(curl -fsSL https://api.github.com/repos/hadolint/hadolint/releases/latest \ + | jq -r --arg name "${hadolint_name}" '.assets[] | select(.name == $name) | .browser_download_url' \ + | head -n 1)"; \ + test -n "${hadolint_url}"; \ + curl -fsSL --retry 3 --retry-delay 2 -o /usr/local/bin/hadolint "${hadolint_url}"; \ + chmod 0755 /usr/local/bin/hadolint; \ + actionlint_suffix="_linux_${actionlint_arch}.tar.gz"; \ + actionlint_url="$(curl -fsSL https://api.github.com/repos/rhysd/actionlint/releases/latest \ + | jq -r --arg suffix "${actionlint_suffix}" '.assets[] | select(.name | endswith($suffix)) | .browser_download_url' \ + | head -n 1)"; \ + test -n "${actionlint_url}"; \ + curl -fsSL --retry 3 --retry-delay 2 -o /tmp/actionlint.tar.gz "${actionlint_url}"; \ + tar -xzf /tmp/actionlint.tar.gz -C /tmp actionlint; \ + install -m 0755 /tmp/actionlint /usr/local/bin/actionlint; \ + rm -f /tmp/actionlint /tmp/actionlint.tar.gz; \ + hadolint --version; \ + actionlint -version + # Copy the pinned Bookworm-built RTK and tokensave binaries and execute them in the final # image. This makes an ABI mismatch fail the image build instead of surfacing at runtime. COPY --from=rtk-builder /out/rtk /usr/local/bin/rtk @@ -107,7 +142,7 @@ COPY --from=tokensave-builder /out/bin/tokensave /usr/local/bin/tokensave RUN /usr/local/bin/rtk --version && /usr/local/bin/tokensave --version # Install only the Headroom proxy, code-compression, and MCP features used by this add-on, -# plus mcp-proxy (stdio->SSE bridge for the Home Assistant MCP server) and uv (fast +# plus mcp-proxy (stdio->HTTP bridge for the Home Assistant MCP server) and uv (fast # installer used for the additional_pip option). RUN apt-get update && \ apt-get install -y --no-install-recommends nodejs && \ @@ -159,7 +194,6 @@ CMD [ "/ha_entrypoint.sh" ] # 5 Labels # ############ -ARG BUILD_ARCH ARG BUILD_DATE ARG BUILD_DESCRIPTION ARG BUILD_NAME From bdd56b8057dd899dfe9718d58366a24447a48391 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:16:08 +0200 Subject: [PATCH 09/18] Improve Claude optimization tooling --- claude_desktop/README.md | 121 +++++++++++++++++++++++++++++---------- 1 file changed, 90 insertions(+), 31 deletions(-) diff --git a/claude_desktop/README.md b/claude_desktop/README.md index 4967a19655..09e9d22eae 100644 --- a/claude_desktop/README.md +++ b/claude_desktop/README.md @@ -4,9 +4,9 @@ ![Supports amd64 Architecture][amd64-shield] ![Project Maintenance][maintenance-shield] -Run Claude Desktop in a LinuxServer.io Selkies add-on, with Headroom MCP -context compression, RTK Bash-output acceleration, and code-intelligence -tooling wired in by default. +Run Claude Desktop in a LinuxServer.io Selkies add-on, with Headroom context +compression, RTK Bash-output acceleration, and TokenSave semantic code +intelligence wired in by default. ## Installation @@ -24,15 +24,33 @@ currently does not include Computer Use or dictation. Everything is built around the Claude Desktop app. Claude Code is installed in the same image but is not exposed as a standalone service: Claude Desktop's cowork and dispatch sessions run it internally, and they pick up the shared -Claude Code configuration (`~/.claude`), hooks, and MCP servers automatically. +Claude Code configuration (`~/.claude`), hooks, MCP servers, and PATH tools. - **Claude Desktop** uses Headroom through its MCP tools. -- **Claude Code sessions inside Desktop** get the same MCP servers via - `~/.claude.json` and RTK's `PreToolUse` Bash hook via - `~/.claude/settings.json`. +- **Claude Code sessions inside Desktop** get the same MCP servers and the + RTK/TokenSave hooks through the shared Claude Code configuration. +- PATH-based Claude Code launches are routed through the supervised Headroom + proxy when `headroom_wrap_claude_code` is enabled. If a Desktop release calls + `/usr/bin/claude` directly, the session remains functional and still has the + Headroom MCP tools, but transparent proxy compression cannot be injected. - **gnome-keyring** provides the Secret Service backend Electron needs to persist sign-in and dispatch permission grants across restarts. +## Optimization layers + +The three bundled optimization tools are complementary: + +- **RTK** rewrites supported Bash commands so Claude receives compact output. +- **TokenSave** builds a local semantic graph for explicitly selected code + repositories and steers Claude away from repeated Explore/Grep/Read fan-out. +- **Headroom** transparently compresses proxied Claude Code traffic and also + exposes on-demand compress/retrieve/statistics MCP tools to Claude Desktop. + +TokenSave's complete Claude integration is installed at startup: MCP server, +permissions, PreToolUse/UserPromptSubmit/Stop hooks, global prompt rules, and +Git synchronization hooks. A repository is indexed only when it is listed in +`tokensave_project_paths`; no automatic filesystem scan is performed. + ## Features - Claude Desktop in single-app Selkies mode with Home Assistant ingress. @@ -42,15 +60,17 @@ Claude Code configuration (`~/.claude`), hooks, and MCP servers automatically. preserving Desktop and Claude Code state across restarts. - Persistent sign-in through a bundled, auto-unlocked gnome-keyring. - Optional runtime Claude Desktop updates from Anthropic's apt repository. -- Optional extra apt and pip package installation (pip installs use `uv` for - speed). -- Baked-in `git`, GitHub CLI (`gh`), and `ripgrep`. +- Optional extra apt and pip package installation (pip installs use `uv`). +- Baked-in `git`, GitHub CLI (`gh`), `ripgrep`, `jq`, `shellcheck`, `yamllint`, + `hadolint`, and `actionlint`. - Custom script support through the repository standard `claude_desktop.sh`. -- Bundled optimization tools: Headroom (MCP + local proxy), RTK, tokensave, - and Caveman — each individually switchable. +- Bundled optimization tools: Headroom, RTK, and TokenSave; Caveman remains + available as an opt-in plugin. - Optional Home Assistant MCP bridge so Claude can query and control Home Assistant. -- Headroom dashboard exposed on mapped port `8787`. +- Independent hourly savings reports for Headroom, RTK, and TokenSave. +- `claude-tools-doctor.sh` diagnostics for binaries, routing, hooks, MCP + registrations, project indexes, proxy health, and gains. - Low-power defaults for GPU mapping, Selkies frame rate, and volatile caches. ## Options @@ -64,42 +84,79 @@ Claude Code configuration (`~/.claude`), hooks, and MCP servers automatically. | `DRINODE` | | Optional GPU device override for Selkies. | | `DNS_server` | `8.8.8.8` | DNS server used by the standard DNS module. | | `auto_update` | `true` | Upgrade `claude-desktop` from Anthropic's apt repository at startup. | -| `install_headroom` | `true` | Register the Headroom MCP server and run the supervised local proxy/dashboard. | -| `install_rtk` | `true` | Configure RTK's Claude Code `PreToolUse` hook. | -| `install_tokensave` | `true` | Register the tokensave code-intelligence MCP server for Desktop and Claude Code. | -| `install_caveman` | `true` | Install the Caveman Claude Code plugin in the persistent Claude home. | +| `install_headroom` | `true` | Register Headroom MCP and run the supervised local proxy. | +| `headroom_wrap_claude_code` | `true` | Route PATH-based Claude Code launches through the already-running Headroom proxy. | +| `expose_headroom_dashboard` | `false` | Bind Headroom to all interfaces. Port `8787/tcp` must also be mapped manually. | +| `install_rtk` | `true` | Configure RTK's Claude Code `PreToolUse` Bash hook. | +| `install_tokensave` | `true` | Install TokenSave's complete global Claude integration. | +| `tokensave_project_paths` | `[]` | Explicit absolute Git repository paths to initialize or sync at startup. | +| `install_caveman` | `false` | Install the third-party Caveman Claude Code plugin at startup. | +| `enable_tools_health_report` | `true` | Write independent Headroom, RTK, and TokenSave gains to the add-on log hourly. | | `install_github_cli` | `true` | Enable setup checks for the baked-in `git` and `gh` commands. | | `github_token` | | Optional GitHub token used to authenticate `gh` and Git operations. | | `github_username` | | Optional global Git author name. | | `github_email` | | Optional global Git author email. | | `enable_ha_mcp` | `false` | Register Home Assistant's MCP server in Claude (requires `ha_mcp_token`). | -| `ha_mcp_url` | `http://homeassistant:8123/mcp_server/sse` | SSE endpoint of Home Assistant's MCP Server integration. | +| `ha_mcp_url` | `http://homeassistant:8123/api/mcp` | Streamable HTTP endpoint of Home Assistant's MCP Server integration. | | `ha_mcp_token` | | Home Assistant long-lived access token used by the MCP bridge. | | `additional_apps` | | Comma-separated Debian apt packages to install at startup. | | `additional_pip` | | Comma-separated pip packages installed at startup (via `uv`). | | `data_location` | `/data/data` | Persistent home directory for Claude and tooling. | | `env_vars` | `[]` | Additional environment variables exported inside the container. | +### TokenSave project example + +Only repositories listed here are indexed. Paths must be absolute, mounted in +the add-on, and resolve to a Git working tree: + +```yaml +tokensave_project_paths: + - /share/projects/hassio-addons + - /share/projects/birdnet-go +``` + +At startup, an uninitialized repository receives `tokensave init`; an existing +index receives an incremental `tokensave sync`. Removing a path from the option +stops automatic synchronization but does not delete its `.tokensave` database. + ## Headroom behavior When `install_headroom` is enabled, the add-on registers `headroom mcp serve` -in Claude Desktop and Claude Code, and starts a supervised local Headroom -backend. Claude can use `headroom_compress`, `headroom_retrieve`, and -`headroom_stats` through MCP. +with the explicit local proxy URL in Claude Desktop and Claude Code, then starts +a supervised Headroom backend on `127.0.0.1:8787`. -Claude Desktop overrides `ANTHROPIC_BASE_URL`, so it is deliberately launched -without proxy injection; the MCP integration is the supported path. +Claude Desktop overrides `ANTHROPIC_BASE_URL`, so Desktop chat deliberately uses +the MCP integration. The `/usr/local/bin/claude` wrapper routes PATH-based Claude +Code sessions through `headroom wrap claude --no-proxy`, reusing the supervised +backend without starting a second proxy. -The Headroom dashboard is available at: +The dashboard is disabled externally by default. To expose it: -```text -http://:8787/dashboard +1. Set `expose_headroom_dashboard: true`. +2. Map `8787/tcp` in the add-on **Network** section. +3. Open `http://:8787/dashboard`. + +The dashboard is unauthenticated. Do not publish this port to the public +internet. + +## Diagnostics + +Run the following inside the add-on through a custom script or container console: + +```bash +claude-tools-doctor.sh ``` -through the default `8787/tcp` port mapping. The dashboard is unauthenticated -and is reachable wherever Home Assistant publishes that port, so treat it as -sensitive: do not expose it directly to the public internet, and unmap the port -in the add-on **Network** section if you do not want it reachable at all. +The report checks the tool binaries, configuration switches, redacted MCP +registrations, Claude hooks, 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: + +```bash +claude-gains-report.sh +``` ## Home Assistant MCP bridge @@ -130,8 +187,10 @@ Persistent state is stored in the configured `data_location` (default - Claude Desktop sign-in: `~/.config/Claude` (token encrypted via gnome-keyring; keyring DB in `~/.local/share/keyrings`) - Claude Code settings, hooks, sessions, and plugins: `~/.claude` -- Headroom, RTK, and tokensave user state: their standard paths below the +- Headroom, RTK, and TokenSave user state: their standard paths below the shared home +- TokenSave repository indexes: `.tokensave/` inside each explicitly configured + project Volatile cache data is redirected to `/tmp/cache` through `$XDG_CACHE_HOME` and `$HOME/.cache`. From 66d3886b800bd6412fd3fc5749be5cdef7d5813a Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:17:05 +0200 Subject: [PATCH 10/18] Improve Claude optimization tooling --- claude_desktop/CHANGELOG.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/claude_desktop/CHANGELOG.md b/claude_desktop/CHANGELOG.md index 65f5bffc77..098c4420bf 100644 --- a/claude_desktop/CHANGELOG.md +++ b/claude_desktop/CHANGELOG.md @@ -1,3 +1,15 @@ +## 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`. +- Add `tokensave_project_paths` for explicit per-repository initialization and incremental synchronization; no repositories are scanned or indexed unless listed. +- Route PATH-based Claude Code launches through the already-supervised Headroom proxy by default with a recursion-safe `/usr/local/bin/claude` wrapper; fall back to the official binary when the proxy is unavailable. +- Pass the local proxy URL explicitly to the Headroom MCP server, while retaining MCP-only integration for the Desktop Electron application. +- Keep the unauthenticated Headroom dashboard container-local by default; add `expose_headroom_dashboard` and leave port `8787/tcp` unmapped until explicitly enabled. +- Fix the hourly gains report so Headroom no longer suppresses RTK output, add TokenSave gains, and gate each tool on its actual add-on option. +- Add `claude-tools-doctor.sh` to inspect binaries, redacted MCP registrations, hooks, proxy health, routing, project indexes, and gains. +- Install local validation tools (`jq`, `shellcheck`, `yamllint`, current `hadolint`, and current `actionlint`) to reduce avoidable CI round-trips. +- Disable the unpinned third-party Caveman startup installer by default; it remains opt-in. + ## 1.19 (14-07-2026) - Minor bugs fixed ## 1.18 (14-07-2026) @@ -17,7 +29,7 @@ - Minor bugs fixed ## 1.15 (13-07-2026) - Minor bugs fixed - + ## ubunturesolute-version-6dc44b0e (2026-07-13) - Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases) ## 1.14 (10-07-2026) From 7cd82b275875eb1fa89d81646d7f8e865e584a57 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:20:43 +0200 Subject: [PATCH 11/18] Fix validator release asset lookup --- claude_desktop/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/claude_desktop/Dockerfile b/claude_desktop/Dockerfile index ccf216a9ed..208f571420 100644 --- a/claude_desktop/Dockerfile +++ b/claude_desktop/Dockerfile @@ -116,7 +116,7 @@ RUN set -eux; \ aarch64) hadolint_arch="arm64"; actionlint_arch="arm64" ;; \ *) echo "Unsupported validation-tools architecture: ${BUILD_ARCH}" >&2; exit 1 ;; \ esac; \ - hadolint_name="hadolint-Linux-${hadolint_arch}"; \ + hadolint_name="hadolint-linux-${hadolint_arch}"; \ hadolint_url="$(curl -fsSL https://api.github.com/repos/hadolint/hadolint/releases/latest \ | jq -r --arg name "${hadolint_name}" '.assets[] | select(.name == $name) | .browser_download_url' \ | head -n 1)"; \ From 45f88307d4ca685ecd069fd2e0bf95f7e78b17bd Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:04:23 +0200 Subject: [PATCH 12/18] Add Claude permission modes --- claude_desktop/config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/claude_desktop/config.yaml b/claude_desktop/config.yaml index d6b4631ec5..b54e4f4ae7 100644 --- a/claude_desktop/config.yaml +++ b/claude_desktop/config.yaml @@ -54,6 +54,7 @@ options: install_headroom: true install_rtk: true install_tokensave: true + permission_mode: auto tokensave_project_paths: [] panel_admin: false panel_icon: mdi:robot-happy @@ -95,6 +96,7 @@ schema: install_headroom: bool install_rtk: bool install_tokensave: bool + permission_mode: list(strict|auto|bypass) tokensave_project_paths: - str slug: claude_desktop From 321f2fde73f6c1a81590ee9f7e4eb0fcfdce7568 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:04:48 +0200 Subject: [PATCH 13/18] Apply Claude permission mode in wrapper --- claude_desktop/rootfs/usr/local/bin/claude | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/claude_desktop/rootfs/usr/local/bin/claude b/claude_desktop/rootfs/usr/local/bin/claude index e4864e1e1e..ac0a27a966 100644 --- a/claude_desktop/rootfs/usr/local/bin/claude +++ b/claude_desktop/rootfs/usr/local/bin/claude @@ -5,6 +5,22 @@ set -o pipefail REAL_CLAUDE="/usr/bin/claude" HEADROOM_BIN="/usr/local/bin/headroom" HEADROOM_URL="http://127.0.0.1:8787" +PERMISSION_MODE="$(bashio::config 'permission_mode')" +declare -a CLAUDE_PERMISSION_ARGS=() + +case "$PERMISSION_MODE" in + bypass) + CLAUDE_PERMISSION_ARGS+=("--dangerously-skip-permissions") + ;; + auto) + CLAUDE_PERMISSION_ARGS+=("--permission-mode" "auto") + ;; + strict|"") + ;; + *) + echo "claude wrapper: unknown permission_mode '${PERMISSION_MODE}', using strict mode" >&2 + ;; +esac if [ ! -x "$REAL_CLAUDE" ]; then echo "claude wrapper: ${REAL_CLAUDE} is unavailable" >&2 @@ -17,9 +33,10 @@ if bashio::config.true 'install_headroom' && bashio::config.true 'headroom_wrap_ # executable; otherwise it would resolve this wrapper recursively. export HEADROOM_CONTEXT_TOOL="rtk" exec env PATH="/usr/bin:/bin:/usr/local/bin" \ - "$HEADROOM_BIN" wrap claude --no-proxy -- "$@" + "$HEADROOM_BIN" wrap claude --no-proxy -- \ + "${CLAUDE_PERMISSION_ARGS[@]}" "$@" fi echo "claude wrapper: Headroom proxy is unavailable; launching Claude Code directly" >&2 fi -exec "$REAL_CLAUDE" "$@" +exec "$REAL_CLAUDE" "${CLAUDE_PERMISSION_ARGS[@]}" "$@" From c76b257f5de71b5dcceb4131c393fcc795309538 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:05:29 +0200 Subject: [PATCH 14/18] Persist Claude permission mode --- .../etc/cont-init.d/83-claude_permissions.sh | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 claude_desktop/rootfs/etc/cont-init.d/83-claude_permissions.sh diff --git a/claude_desktop/rootfs/etc/cont-init.d/83-claude_permissions.sh b/claude_desktop/rootfs/etc/cont-init.d/83-claude_permissions.sh new file mode 100644 index 0000000000..11c3affb22 --- /dev/null +++ b/claude_desktop/rootfs/etc/cont-init.d/83-claude_permissions.sh @@ -0,0 +1,92 @@ +#!/usr/bin/with-contenv bashio +# shellcheck shell=bash +set -e +set -o pipefail + +PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else echo '0'; fi)" +PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '0'; fi)" +PERMISSION_MODE="$(bashio::config 'permission_mode')" +SETTINGS_PATH="$HOME/.claude/settings.json" +STATE_PATH="$HOME/.claude/.addon-permission-mode.json" + +case "$PERMISSION_MODE" in + strict|auto|bypass) ;; + *) + bashio::log.warning "Unknown permission_mode '${PERMISSION_MODE}'; falling back to strict" + PERMISSION_MODE="strict" + ;; +esac + +mkdir -p "$(dirname "$SETTINGS_PATH")" +PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" STATE_PATH="$STATE_PATH" python3 - <<'PY' +import json +import os +from pathlib import Path + +mode = os.environ["PERMISSION_MODE"] +settings_path = Path(os.environ["SETTINGS_PATH"]) +state_path = Path(os.environ["STATE_PATH"]) + +try: + settings = json.loads(settings_path.read_text()) if settings_path.exists() else {} +except (OSError, json.JSONDecodeError): + if settings_path.exists(): + settings_path.rename(settings_path.with_suffix(settings_path.suffix + ".bak")) + settings = {} +if not isinstance(settings, dict): + settings = {} + +try: + state = json.loads(state_path.read_text()) if state_path.exists() else None +except (OSError, json.JSONDecodeError): + state = None +if not isinstance(state, dict): + state = None + +permissions = settings.get("permissions") +if not isinstance(permissions, dict): + permissions = {} + +if mode == "strict": + # Restore the value that existed before the add-on first managed this setting. + if state is not None: + if state.get("previous_exists"): + permissions["defaultMode"] = state.get("previous_value") + else: + permissions.pop("defaultMode", None) + state_path.unlink(missing_ok=True) +else: + if state is None: + state = { + "previous_exists": "defaultMode" in permissions, + "previous_value": permissions.get("defaultMode"), + } + state_path.write_text(json.dumps(state, indent=2) + "\n") + state_path.chmod(0o600) + permissions["defaultMode"] = "auto" if mode == "auto" else "bypassPermissions" + +if permissions: + settings["permissions"] = permissions +else: + settings.pop("permissions", None) + +settings_path.write_text(json.dumps(settings, indent=2) + "\n") +settings_path.chmod(0o600) +PY + +case "$PERMISSION_MODE" in + strict) + bashio::log.info "Claude Code permission mode: strict (normal prompts)" + ;; + auto) + bashio::log.info "Claude Code permission mode: auto (safe actions approved automatically)" + ;; + bypass) + bashio::log.warning "Claude Code permission mode: bypass (permission checks disabled for mounted data and available tools)" + ;; +esac + +chown -- "${PUID}:${PGID}" "$SETTINGS_PATH" 2> /dev/null || true +if [ -e "$STATE_PATH" ]; then + chown -- "${PUID}:${PGID}" "$STATE_PATH" 2> /dev/null || true +fi From d1ceafebe8eb3c81b8b842d65d5634cdcc4b25ee Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:08:08 +0200 Subject: [PATCH 15/18] Fix actionlint amd64 asset mapping --- claude_desktop/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/claude_desktop/Dockerfile b/claude_desktop/Dockerfile index 208f571420..e8bff0593f 100644 --- a/claude_desktop/Dockerfile +++ b/claude_desktop/Dockerfile @@ -112,7 +112,7 @@ RUN install -d -m 0755 /etc/apt/keyrings && \ # developer tools are intentionally not version-pinned. RUN set -eux; \ case "${BUILD_ARCH}" in \ - amd64) hadolint_arch="x86_64"; actionlint_arch="x86_64" ;; \ + amd64) hadolint_arch="x86_64"; actionlint_arch="amd64" ;; \ aarch64) hadolint_arch="arm64"; actionlint_arch="arm64" ;; \ *) echo "Unsupported validation-tools architecture: ${BUILD_ARCH}" >&2; exit 1 ;; \ esac; \ From 15505a265ff64168720551022415c05cdb01363d Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:08:34 +0200 Subject: [PATCH 16/18] Trust configured TokenSave repositories --- .../cont-init.d/81-tokensave_repositories.sh | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 claude_desktop/rootfs/etc/cont-init.d/81-tokensave_repositories.sh diff --git a/claude_desktop/rootfs/etc/cont-init.d/81-tokensave_repositories.sh b/claude_desktop/rootfs/etc/cont-init.d/81-tokensave_repositories.sh new file mode 100644 index 0000000000..4b36dbc397 --- /dev/null +++ b/claude_desktop/rootfs/etc/cont-init.d/81-tokensave_repositories.sh @@ -0,0 +1,36 @@ +#!/usr/bin/with-contenv bashio +# shellcheck shell=bash +set -e +set -o pipefail + +if ! bashio::config.true 'install_tokensave' || ! command -v git > /dev/null 2>&1; then + exit 0 +fi + +declare -A REPOS_SEEN=() +while IFS= read -r configured_path; do + configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}" + configured_path="${configured_path%"${configured_path##*[![:space:]]}"}" + [ -n "$configured_path" ] || continue + + case "$configured_path" in + /*) ;; + *) continue ;; + esac + [ -d "$configured_path" ] || continue + + # The one-shot safe.directory override is used only to discover the repository root. + # Persist the resolved root in the shared runtime user's Git config before 82-claude_tools.sh + # performs normal repository detection, avoiding Git's dubious-ownership rejection. + repo_root="$(s6-setuidgid abc env HOME="$HOME" \ + git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)" + [ -n "$repo_root" ] && [ "$repo_root" != "/" ] || continue + [[ -z "${REPOS_SEEN[$repo_root]:-}" ]] || continue + REPOS_SEEN[$repo_root]=1 + + if ! s6-setuidgid abc env HOME="$HOME" git config --global --get-all safe.directory \ + | grep -Fxq -- "$repo_root"; then + 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}" + fi +done < <(bashio::config.array 'tokensave_project_paths') From 8073317150428d220c57649df661329d7da37a73 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:09:44 +0200 Subject: [PATCH 17/18] Document Claude permission modes --- claude_desktop/README.md | 43 +++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/claude_desktop/README.md b/claude_desktop/README.md index 09e9d22eae..33cf4208e7 100644 --- a/claude_desktop/README.md +++ b/claude_desktop/README.md @@ -24,15 +24,17 @@ currently does not include Computer Use or dictation. Everything is built around the Claude Desktop app. Claude Code is installed in the same image but is not exposed as a standalone service: Claude Desktop's cowork and dispatch sessions run it internally, and they pick up the shared -Claude Code configuration (`~/.claude`), hooks, MCP servers, and PATH tools. +Claude Code configuration (`~/.claude`), hooks, MCP servers, permissions, and +PATH tools. - **Claude Desktop** uses Headroom through its MCP tools. -- **Claude Code sessions inside Desktop** get the same MCP servers and the - RTK/TokenSave hooks through the shared Claude Code configuration. +- **Claude Code sessions inside Desktop** get the same MCP servers, permission + mode, and RTK/TokenSave hooks through the shared Claude Code configuration. - PATH-based Claude Code launches are routed through the supervised Headroom proxy when `headroom_wrap_claude_code` is enabled. If a Desktop release calls `/usr/bin/claude` directly, the session remains functional and still has the - Headroom MCP tools, but transparent proxy compression cannot be injected. + shared permission mode and Headroom MCP tools, but transparent proxy + compression cannot be injected. - **gnome-keyring** provides the Secret Service backend Electron needs to persist sign-in and dispatch permission grants across restarts. @@ -59,6 +61,8 @@ Git synchronization hooks. A repository is indexed only when it is listed in - Persistent `$HOME` at the configured `data_location` (default `/data/data`), preserving Desktop and Claude Code state across restarts. - 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. - 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`, @@ -70,7 +74,7 @@ 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, and gains. + registrations, project indexes, proxy health, permissions, and gains. - Low-power defaults for GPU mapping, Selkies frame rate, and volatile caches. ## Options @@ -84,6 +88,7 @@ Git synchronization hooks. A repository is indexed only when it is listed in | `DRINODE` | | Optional GPU device override for Selkies. | | `DNS_server` | `8.8.8.8` | DNS server used by the standard DNS module. | | `auto_update` | `true` | Upgrade `claude-desktop` from Anthropic's apt repository at startup. | +| `permission_mode` | `auto` | Claude Code permission policy: `strict`, `auto`, or `bypass`. | | `install_headroom` | `true` | Register Headroom MCP and run the supervised local proxy. | | `headroom_wrap_claude_code` | `true` | Route PATH-based Claude Code launches through the already-running Headroom proxy. | | `expose_headroom_dashboard` | `false` | Bind Headroom to all interfaces. Port `8787/tcp` must also be mapped manually. | @@ -104,6 +109,23 @@ Git synchronization hooks. A repository is indexed only when it is listed in | `data_location` | `/data/data` | Persistent home directory for Claude and tooling. | | `env_vars` | `[]` | Additional environment variables exported inside the container. | +### Permission modes + +```yaml +permission_mode: auto +``` + +- `strict` keeps Claude Code's normal interactive permission prompts. +- `auto` asks Claude Code's automatic permission classifier to approve safe + operations while retaining prompts for risky actions. This is the default. +- `bypass` disables Claude Code permission checks by using + `bypassPermissions` in the shared settings and + `--dangerously-skip-permissions` for wrapper-launched sessions. + +`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. + ### TokenSave project example Only repositories listed here are indexed. Paths must be absolute, mounted in @@ -118,6 +140,8 @@ tokensave_project_paths: At startup, an uninitialized repository receives `tokensave init`; an existing index receives an incremental `tokensave sync`. Removing a path from the option stops automatic synchronization but does not delete its `.tokensave` database. +Configured repositories are added to Git's `safe.directory` list for the shared +runtime user before TokenSave performs repository discovery. ## Headroom behavior @@ -148,9 +172,9 @@ claude-tools-doctor.sh ``` The report checks the tool binaries, configuration switches, redacted MCP -registrations, Claude hooks, 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. +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: @@ -186,7 +210,8 @@ Persistent state is stored in the configured `data_location` (default - Claude Desktop sign-in: `~/.config/Claude` (token encrypted via gnome-keyring; keyring DB in `~/.local/share/keyrings`) -- Claude Code settings, hooks, sessions, and plugins: `~/.claude` +- Claude Code settings, hooks, sessions, plugins, and permission mode: + `~/.claude` - Headroom, RTK, and TokenSave user state: their standard paths below the shared home - TokenSave repository indexes: `.tokensave/` inside each explicitly configured From a38bc75f95db791e4ccfd270be259c82a047bd22 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:11:00 +0200 Subject: [PATCH 18/18] Report Claude permission mode in diagnostics --- .../usr/local/bin/claude-tools-doctor.sh | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) 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 0f16921f05..9ae7dda7da 100644 --- a/claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh +++ b/claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh @@ -1,5 +1,5 @@ #!/usr/bin/with-contenv bashio -# Diagnose installation, registration, routing, indexing, and recorded savings without +# Diagnose installation, registration, routing, indexing, permissions, and recorded savings without # printing MCP environment values (which may contain the Home Assistant access token). # shellcheck shell=bash set +e @@ -22,10 +22,31 @@ for tool in claude claude-desktop headroom rtk tokensave git gh rg jq shellcheck done section "Configured switches" -for option in install_headroom headroom_wrap_claude_code expose_headroom_dashboard install_rtk install_tokensave install_caveman enable_tools_health_report; do +for option in permission_mode install_headroom headroom_wrap_claude_code expose_headroom_dashboard install_rtk install_tokensave install_caveman enable_tools_health_report; do printf '%-30s %s\n' "$option" "$(bashio::config "$option")" done +section "Claude Code permission state" +python3 - <<'PY' +import json +from pathlib import Path + +path = Path.home() / ".claude/settings.json" +try: + data = json.loads(path.read_text()) +except FileNotFoundError: + print("settings: MISSING") +except Exception as exc: + print(f"settings: INVALID: {exc}") +else: + permissions = data.get("permissions", {}) + if isinstance(permissions, dict): + print(f"permissions.defaultMode: {permissions.get('defaultMode', '')}") + else: + print("permissions: INVALID") +print(f"managed-state marker: {(Path.home() / '.claude/.addon-permission-mode.json').exists()}") +PY + section "MCP registrations (environment values redacted)" python3 - <<'PY' import json @@ -118,11 +139,11 @@ if bashio::config.true 'install_tokensave'; then tokensave gain --all --range 30d || true while IFS= read -r configured_path; do [ -n "$configured_path" ] || continue - repo_root="$(git -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 echo "${configured_path}: not a Git repository" elif [ -f "$repo_root/.tokensave/tokensave.db" ]; then - tokensave status "$repo_root" --short || true + s6-setuidgid abc env HOME="$HOME" tokensave status "$repo_root" --short || true else echo "${repo_root}: NOT INITIALIZED" fi