From b56be1f57da822c391018e48ee987bde0fd52a4e Mon Sep 17 00:00:00 2001 From: alexbelgium Date: Tue, 14 Jul 2026 16:27:52 +0200 Subject: [PATCH] feat(claude_desktop): desktop-only architecture, fix dashboard + dispatch Remove standalone web terminal (ttyd/tmux service, port 7681, terminal_* options, claude-direct/claude-headroom wrappers). Claude Code stays and powers Desktop cowork/dispatch sessions. Fix Headroom dashboard: proxy bound 127.0.0.1 only, mapped port 8787 refused external connections; bind 0.0.0.0. Fix dispatch/sign-in persistence: gnome-keyring package was never installed, so the autostart keyring bootstrap no-oped and Electron safeStorage was unavailable (allowlist cache + auth grants lost). Add tokensave MCP (pinned 7.2.0, source-built like RTK), real HA MCP bridge via mcp-proxy (enable_ha_mcp + ha_mcp_url/ha_mcp_token), uv for additional_pip. Register managed MCP servers in Desktop and Claude Code configs without clobbering user entries. Drop orphan options ha_smart_context/dangerously_skip_permissions. Co-Authored-By: Claude Opus 4.8 --- claude_desktop/CHANGELOG.md | 11 ++ claude_desktop/Dockerfile | 45 +++-- claude_desktop/README.md | 170 +++++++----------- claude_desktop/SIGN_IN.md | 10 +- claude_desktop/config.yaml | 26 +-- claude_desktop/rootfs/defaults/autostart | 3 +- .../etc/cont-init.d/80-configuration.sh | 7 +- .../rootfs/etc/cont-init.d/82-claude_tools.sh | 144 +++++++++++---- .../dependencies.d/init-services | 0 .../s6-rc.d/svc-claude-terminal/run | 101 ----------- .../s6-rc.d/svc-claude-terminal/type | 1 - .../etc/s6-overlay/s6-rc.d/svc-headroom/run | 4 +- .../user/contents.d/svc-claude-terminal | 0 .../rootfs/usr/local/bin/claude-direct | 4 - .../rootfs/usr/local/bin/claude-headroom | 21 --- .../usr/local/bin/claude-terminal-shell | 24 --- 16 files changed, 223 insertions(+), 348 deletions(-) delete mode 100644 claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/dependencies.d/init-services delete mode 100644 claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/run delete mode 100644 claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/type delete mode 100644 claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-claude-terminal delete mode 100644 claude_desktop/rootfs/usr/local/bin/claude-direct delete mode 100644 claude_desktop/rootfs/usr/local/bin/claude-headroom delete mode 100644 claude_desktop/rootfs/usr/local/bin/claude-terminal-shell diff --git a/claude_desktop/CHANGELOG.md b/claude_desktop/CHANGELOG.md index eea101b70c..bac60d1043 100644 --- a/claude_desktop/CHANGELOG.md +++ b/claude_desktop/CHANGELOG.md @@ -1,3 +1,14 @@ +## 1.17 (14-07-2026) + +- **Breaking:** remove the standalone Claude Code web terminal (ttyd/tmux service, port `7681`, and the `enable_terminal`, `terminal_username`, `terminal_password`, `terminal_workspace` options). The add-on is now built purely around Claude Desktop; Claude Code remains installed and powers Desktop cowork/dispatch sessions with the RTK hook, Caveman, and MCP servers intact. If the add-on refuses to start after the update, open its Configuration tab and re-save to drop the removed options. +- Remove the `claude-direct` and `claude-headroom` terminal wrapper scripts and the unused `ha_smart_context` and `dangerously_skip_permissions` options. +- Fix the Headroom dashboard being unreachable at `http://:8787/dashboard`: the supervised proxy only listened on `127.0.0.1`; it now binds `0.0.0.0` so the mapped port works. +- Fix dispatch/remote sessions and sign-in persistence: install the missing `gnome-keyring` package. The existing keyring bootstrap silently no-oped without it, leaving Electron `safeStorage` unavailable ("cannot store allowlist cache"), so auth tokens and dispatch permission grants were lost on restart. +- Add the tokensave code-intelligence MCP server (pinned 7.2.0, built from source like RTK), registered for both Claude Desktop and Claude Code; disable with `install_tokensave: false`. +- Implement the Home Assistant MCP bridge for real: `enable_ha_mcp` plus new `ha_mcp_url`/`ha_mcp_token` options register Home Assistant's MCP Server integration in Claude through `mcp-proxy`. +- Register add-on-managed MCP servers in Claude Code's `~/.claude.json` as well as Claude Desktop's config, without clobbering user-customized entries. +- Install `uv` and use it for the `additional_pip` option for much faster package installs. + ## 1.16 (14-07-2026) - Minor bugs fixed ## 1.15 (13-07-2026) diff --git a/claude_desktop/Dockerfile b/claude_desktop/Dockerfile index 722a7b9e89..1157d6d62e 100644 --- a/claude_desktop/Dockerfile +++ b/claude_desktop/Dockerfile @@ -11,6 +11,7 @@ ARG BUILD_FROM ARG BUILD_VERSION ARG RTK_VERSION="v0.43.0" ARG RTK_COMMIT="5a7880d404db8364d602f2ecdc41dd790f64013f" +ARG TOKENSAVE_VERSION="7.2.0" # The upstream aarch64 release is cross-built on ubuntu-latest and requires # GLIBC 2.39. Build the pinned source on Bookworm instead so it is compatible @@ -25,6 +26,13 @@ RUN git clone --depth 1 --branch "${RTK_VERSION}" https://github.com/rtk-ai/rtk. install -D -m 0755 target/release/rtk /out/rtk && \ /out/rtk --version +# tokensave ships no Bookworm-compatible prebuilt binary either; build the pinned +# crates.io release from source so GLIBC matches the add-on runtime. +FROM rust:1.91-bookworm AS tokensave-builder +ARG TOKENSAVE_VERSION +RUN cargo install tokensave --version "${TOKENSAVE_VERSION}" --locked --root /out && \ + /out/bin/tokensave --version + FROM ${BUILD_FROM} ################## @@ -63,15 +71,15 @@ RUN curl -fsSL --retry 3 --retry-delay 2 \ # Add rootfs COPY rootfs/ / -RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \; && \ - chmod +x /usr/local/bin/claude-direct /usr/local/bin/claude-headroom /usr/local/bin/claude-terminal-shell +RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \; # 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, browser-terminal tooling, and Python tooling +# 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. 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 && \ @@ -82,38 +90,27 @@ RUN install -d -m 0755 /etc/apt/keyrings && \ claude-desktop \ claude-code \ python3-pip \ + gnome-keyring \ libsecret-1-0 \ dbus-x11 \ git \ gh \ - ripgrep \ - tmux && \ + ripgrep && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* -# ttyd is not packaged in Debian bookworm. Install the pinned upstream static binary. -ARG TTYD_VERSION="1.7.7" -RUN set -eux; \ - case "$(dpkg --print-architecture)" in \ - amd64) ttyd_arch="x86_64" ;; \ - arm64) ttyd_arch="aarch64" ;; \ - *) echo "Unsupported architecture for ttyd: $(dpkg --print-architecture)" >&2; exit 1 ;; \ - esac; \ - curl -fsSL --retry 3 --retry-delay 2 \ - -o /usr/local/bin/ttyd \ - "https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.${ttyd_arch}"; \ - chmod 0755 /usr/local/bin/ttyd; \ - /usr/local/bin/ttyd --version - -# Copy the pinned Bookworm-built RTK binary and execute it in the final image. -# This makes an ABI mismatch fail the image build instead of surfacing at runtime. +# 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 -RUN /usr/local/bin/rtk --version +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. +# 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 +# installer used for the additional_pip option). RUN apt-get update && \ apt-get install -y --no-install-recommends nodejs && \ - pip3 install --break-system-packages "headroom-ai[proxy,code,mcp]" && \ + pip3 install --break-system-packages "headroom-ai[proxy,code,mcp]" mcp-proxy uv && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* /root/.cache diff --git a/claude_desktop/README.md b/claude_desktop/README.md index 69d75b8ebe..bcb3064c8b 100644 --- a/claude_desktop/README.md +++ b/claude_desktop/README.md @@ -4,8 +4,9 @@ ![Supports amd64 Architecture][amd64-shield] ![Project Maintenance][maintenance-shield] -Run Claude Desktop and an optional persistent Claude Code web terminal in one -LinuxServer.io Selkies add-on. +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. ## Installation @@ -20,102 +21,38 @@ currently does not include Computer Use or dictation. ## Architecture -Claude Desktop and Claude Code run as separate clients inside the same add-on. -They share the configured persistent home directory, Git credentials, -repositories, Claude Code configuration, Headroom storage, and RTK -configuration, but they do not share or hand off a conversation. +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 Desktop** uses Headroom through its MCP tools. -- **Claude Code** uses Headroom's supported `headroom wrap claude` integration. -- **RTK** filters Claude Code Bash output through its `PreToolUse` hook. -- **tmux** keeps the terminal session running when the browser disconnects. +- **Claude Code sessions inside Desktop** get the same MCP servers via + `~/.claude.json` and RTK's `PreToolUse` Bash hook via + `~/.claude/settings.json`. +- **gnome-keyring** provides the Secret Service backend Electron needs to + persist sign-in and dispatch permission grants across restarts. ## Features -- Claude Desktop in single-app Selkies mode. -- Home Assistant ingress support for Claude Desktop. -- Official Claude Code stable package installed in the same image. -- Optional authenticated `ttyd` web terminal on port `7681`. -- Persistent `tmux` session shared by reconnecting terminal clients. +- Claude Desktop in single-app Selkies mode with Home Assistant ingress. +- Official Claude Code stable package powering Desktop cowork/dispatch + sessions. - 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. - Optional runtime Claude Desktop updates from Anthropic's apt repository. -- Optional extra apt and pip package installation. -- Baked-in `git`, GitHub CLI (`gh`), `ripgrep`, and terminal tooling. +- Optional extra apt and pip package installation (pip installs use `uv` for + speed). +- Baked-in `git`, GitHub CLI (`gh`), and `ripgrep`. - Custom script support through the repository standard `claude_desktop.sh`. -- Optional bundled Claude Code optimization tools: Headroom, RTK, and Caveman. -- Headroom dashboard exposed on mapped port `8787` when enabled. +- Bundled optimization tools: Headroom (MCP + local proxy), RTK, tokensave, + and Caveman — each individually switchable. +- Optional Home Assistant MCP bridge so Claude can query and control Home + Assistant. +- Headroom dashboard exposed on mapped port `8787`. - Low-power defaults for GPU mapping, Selkies frame rate, and volatile caches. -## Claude Code terminal setup - -The terminal service is enabled in the add-on configuration but remains -unavailable until authentication is configured. Port `7681` is not mapped by -default. - -1. Set a unique `terminal_password`. The existing `PASSWORD` option is accepted - only as a compatibility fallback. -2. Optionally set `terminal_username` and `terminal_workspace`. -3. Map container port `7681` to a host port in the add-on **Network** section. -4. Restart the add-on. -5. Reach `http://:7681` only through an encrypted VPN or an - HTTPS reverse proxy, then sign in with the configured terminal credentials. - -The terminal opens in a persistent tmux session. Closing the browser detaches -from tmux rather than terminating commands that are already running. - -Start the optimized Claude Code path with: - -```shell -claude-headroom -``` - -This reuses the supervised Headroom proxy on `127.0.0.1:8787` and launches -Claude Code with the required routing. Headroom is told not to install RTK -because the add-on already maintains the RTK hook in -`~/.claude/settings.json`. - -To bypass Headroom for troubleshooting, run: - -```shell -claude-direct -``` - -Running `claude` directly is equivalent to the direct path. The first Claude -Code launch may require its own account authentication; Desktop and Claude Code -store separate client credentials even though both use the configured -persistent home directory. - -### Multiple concurrent clients - -Every browser connection attaches to the same tmux session. Concurrent clients -therefore see the same terminal, keystrokes, and resize events. This is useful -for reconnecting to one long-running session, but it is not an isolated -multi-user terminal. - -### Terminal user and permissions - -The service drops privileges to the LinuxServer `abc` account before starting -ttyd. The effective numeric UID and GID follow the configured `PUID` and `PGID`. -Using `PUID: 0` can provide root-equivalent access inside the add-on; use a -non-zero UID/GID where your storage permissions allow it. - -The configured workspace must resolve to the persistent home directory or a -subdirectory of `/share`, `/media`, `/mnt`, `/data`, or `/config`. Existing -directories are never re-owned by the terminal service and must already be -readable, writable, and searchable by `abc`. - -### Terminal security - -The direct ttyd endpoint uses HTTP Basic Authentication without TLS. -Credentials and terminal traffic are unencrypted on the network. ttyd also -receives its Basic Authentication credential as a process argument, so it is -visible to processes with sufficient access inside the container. - -Do not expose port `7681` directly to the public internet. Use a VPN such as -WireGuard or Tailscale, or place the endpoint behind an HTTPS reverse proxy. -Use a unique `terminal_password` rather than reusing the Selkies `PASSWORD`. - ## Options | Option | Default | Description | @@ -123,48 +60,60 @@ Use a unique `terminal_password` rather than reusing the Selkies `PASSWORD`. | `PUID` / `PGID` | `0` / `0` | Numeric user and group applied by the LinuxServer initialization. | | `TZ` | | Optional timezone, for example `Europe/Brussels`. | | `KEYBOARD` | | Optional Selkies keyboard layout. | -| `PASSWORD` | | Optional password for direct Selkies ports and compatibility fallback for terminal authentication. | +| `PASSWORD` | | Optional password for direct Selkies ports. | | `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. | -| `enable_terminal` | `true` | Enable the supervised Claude Code web-terminal service. | -| `terminal_username` | `claude` | Username used by ttyd Basic Authentication. | -| `terminal_password` | | Dedicated terminal password. The service idles when this and `PASSWORD` are empty. | -| `terminal_workspace` | | Initial directory; defaults to `/workspace`. | -| `install_headroom` | `true` | Enable Headroom MCP for Desktop and the supervised local proxy reused by `claude-headroom`. | +| `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_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. | -| `ha_smart_context` | `true` | Enable Home Assistant smart context support for Claude tooling. | -| `enable_ha_mcp` | `true` | Enable Home Assistant MCP support for Claude tooling. | -| `dangerously_skip_permissions` | `false` | Reserved compatibility option; it is not applied by the terminal launcher. | +| `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_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 with `--break-system-packages`. | -| `data_location` | `/data/data` | Persistent home directory for both Claude clients and tooling. | +| `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. | ## Headroom behavior -When `install_headroom` is enabled, the add-on registers `headroom mcp serve` in -Claude Desktop and starts a supervised local Headroom backend. Desktop can use -`headroom_compress`, `headroom_retrieve`, and `headroom_stats` through MCP. +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. Claude Desktop overrides `ANTHROPIC_BASE_URL`, so it is deliberately launched -without proxy injection. The web terminal instead provides `claude-headroom`, -which reuses the supervised proxy through Headroom's `--no-proxy` mode. RTK -setup remains owned by the add-on through Headroom's `--no-rtk` mode. +without proxy injection; the MCP integration is the supported path. -The Headroom dashboard remains available at: +The Headroom dashboard is available at: ```text http://:8787/dashboard ``` -when the `8787/tcp` port is mapped. Treat this endpoint as sensitive and do not -expose it directly to the public internet. +through the default `8787/tcp` port mapping. Treat this endpoint as sensitive: +it serves your local network only — 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. + +## Home Assistant MCP bridge + +To let Claude query and control Home Assistant: + +1. In Home Assistant, add the **Model Context Protocol Server** integration + (Settings → Devices & services → Add integration). +2. Create a long-lived access token (your profile → Security). +3. Set `enable_ha_mcp: true` and paste the token into `ha_mcp_token` in the + add-on configuration, then restart the add-on. + +The add-on bridges Claude to the integration's SSE endpoint with `mcp-proxy`. +Override `ha_mcp_url` only if your Home Assistant instance is not reachable as +`homeassistant:8123` from add-ons. ## Custom scripts @@ -178,10 +127,11 @@ the image. Persistent state is stored in the configured `data_location` (default `/data/data`): -- Claude Desktop sign-in: `~/.config/Claude` +- 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` -- Default terminal workspace: `~/workspace` -- Headroom and RTK user state: their standard paths below the shared home +- Headroom, RTK, and tokensave user state: their standard paths below the + shared home Volatile cache data is redirected to `/tmp/cache` through `$XDG_CACHE_HOME` and `$HOME/.cache`. diff --git a/claude_desktop/SIGN_IN.md b/claude_desktop/SIGN_IN.md index 8bd12589e7..3e7c007565 100644 --- a/claude_desktop/SIGN_IN.md +++ b/claude_desktop/SIGN_IN.md @@ -4,7 +4,10 @@ Two related sign-in problems when Claude Desktop runs inside the LinuxServer Sel streamed desktop. **Status:** -- **Shipped:** Problem B (keyring persistence) is implemented in v1.4 (Dockerfile + `rootfs/defaults/autostart`). +- **Shipped:** Problem B (keyring persistence) — the `autostart` bootstrap landed in v1.4, but + the `gnome-keyring` package itself was missing from the image until v1.17 (the bootstrap + silently no-oped and Electron logged "safeStorage encryption is not available"). Fixed in + v1.17: the Dockerfile now installs `gnome-keyring`. - **Planned only:** Problem A (in-desktop browser for OAuth) is intentionally not implemented. The image ships no browser; complete the login with the user-side workaround below. @@ -50,8 +53,9 @@ magic link into the in-session Chromium (not a phone). ### User-side workaround (no rebuild) - Add-on Configuration → `additional_apps: chromium`, restart (installed by `rootfs/etc/cont-init.d/80-configuration.sh`). -- Run the two `xdg-settings`/`xdg-mime` commands once in an in-session terminal, or add them - to the custom script `/addon_configs/db21ed7f_claude-desktop/claude-desktop.sh`. +- Add the two `xdg-settings`/`xdg-mime` commands to the custom script + `/addon_configs/db21ed7f_claude-desktop/claude-desktop.sh` (the image ships no standalone + terminal). --- diff --git a/claude_desktop/config.yaml b/claude_desktop/config.yaml index dbd6559f95..86cea61bdf 100644 --- a/claude_desktop/config.yaml +++ b/claude_desktop/config.yaml @@ -2,7 +2,7 @@ arch: - aarch64 - amd64 audio: true -description: "Claude Desktop and a persistent Claude Code web terminal" +description: "Claude Desktop with Headroom MCP context compression and RTK acceleration" devices: - /dev/dri - /dev/dri/card0 @@ -41,28 +41,23 @@ options: additional_pip: "" auto_update: true github_email: "" - ha_smart_context: true - enable_ha_mcp: true - dangerously_skip_permissions: false - enable_terminal: true - terminal_username: claude - terminal_password: "" - terminal_workspace: "" + enable_ha_mcp: false + ha_mcp_url: http://homeassistant:8123/mcp_server/sse + ha_mcp_token: "" github_token: "" github_username: "" install_caveman: true install_github_cli: true install_headroom: true install_rtk: true + install_tokensave: true panel_admin: false panel_icon: mdi:robot-happy ports: 3001/tcp: null - 7681/tcp: 7681 8787/tcp: 8787 ports_description: 3001/tcp: Claude Desktop web interface - 7681/tcp: Claude Code web terminal over HTTP Basic auth (no TLS) 8787/tcp: Headroom dashboard and proxy privileged: - SYS_ADMIN @@ -83,22 +78,19 @@ schema: additional_pip: str? auto_update: bool? github_email: str? - ha_smart_context: bool? enable_ha_mcp: bool? - dangerously_skip_permissions: bool? - enable_terminal: bool? - terminal_username: match(^[A-Za-z0-9_.-]+$)? - terminal_password: password? - terminal_workspace: str? + ha_mcp_url: str? + ha_mcp_token: password? github_token: password? github_username: str? install_caveman: bool install_github_cli: bool install_headroom: bool install_rtk: bool + install_tokensave: bool slug: claude_desktop tmpfs: true udev: true url: https://github.com/alexbelgium/hassio-addons -version: "1.16" +version: "1.17" video: true diff --git a/claude_desktop/rootfs/defaults/autostart b/claude_desktop/rootfs/defaults/autostart index 45f4fd0399..a2ee4e6ad4 100644 --- a/claude_desktop/rootfs/defaults/autostart +++ b/claude_desktop/rootfs/defaults/autostart @@ -22,8 +22,7 @@ else fi # Headroom is intentionally not injected into the Desktop process. Claude Desktop overrides -# ANTHROPIC_BASE_URL, so Desktop uses the registered Headroom MCP tools instead. The Claude Code -# terminal uses the supported `headroom wrap claude` integration through claude-headroom. +# ANTHROPIC_BASE_URL, so Desktop uses the registered Headroom MCP tools instead. # Launch the configured command. If a custom/wrapped command fails to start, fall back to # the plain Claude Desktop launch so the app always comes up for the user. diff --git a/claude_desktop/rootfs/etc/cont-init.d/80-configuration.sh b/claude_desktop/rootfs/etc/cont-init.d/80-configuration.sh index d174a920db..1e920d46fa 100755 --- a/claude_desktop/rootfs/etc/cont-init.d/80-configuration.sh +++ b/claude_desktop/rootfs/etc/cont-init.d/80-configuration.sh @@ -24,7 +24,12 @@ fi if bashio::config.has_value 'additional_pip'; then for p in $(bashio::config 'additional_pip' | tr ',' ' '); do bashio::log.green "... pip: $p" - pip3 install --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed" + # Prefer uv (much faster resolver/installer); fall back to pip3 when unavailable. + if command -v uv &> /dev/null; then + uv pip install --system --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed" + else + pip3 install --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed" + fi done fi 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 9484a20c04..3246dc2bae 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 @@ -15,54 +15,120 @@ printf '%s\n' "$DEFAULT_CLAUDE_DESKTOP_COMMAND" > "$CLAUDE_DESKTOP_COMMAND_FILE" # 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. Register it in Claude Desktop's MCP config, leaving the -# plain launch untouched. The merge is idempotent and preserves any other MCP servers. +# headroom_stats tools inside the app. +# +# 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). +# The merge is idempotent, preserves any other MCP servers, never overwrites a user-customized +# entry with a different command, and removes only add-on-managed entries when disabled. CLAUDE_DESKTOP_CONFIG="$HOME/.config/Claude/claude_desktop_config.json" +CLAUDE_CODE_CONFIG="$HOME/.claude.json" + +HEADROOM_ENABLED=false if bashio::config.true 'install_headroom'; then if command -v headroom &> /dev/null; then - bashio::log.info "headroom $(headroom --version 2> /dev/null || true) available; registering the headroom MCP server for Claude Desktop" - HEADROOM_BIN="$(command -v headroom)" CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" python3 - <<'PY' || bashio::log.warning "Unable to register the headroom MCP server automatically" -import json -import os -from pathlib import Path - -path = Path(os.environ["CLAUDE_DESKTOP_CONFIG"]) -try: - data = json.loads(path.read_text()) if path.exists() else {} - if not isinstance(data, dict): - data = {} -except Exception: - if path.exists(): - path.rename(path.with_suffix(path.suffix + ".bak")) - data = {} -servers = data.get("mcpServers") -if not isinstance(servers, dict): - servers = {} - data["mcpServers"] = servers -servers["headroom"] = {"command": os.environ.get("HEADROOM_BIN", "headroom"), "args": ["mcp", "serve"]} -path.parent.mkdir(parents=True, exist_ok=True) -path.write_text(json.dumps(data, indent=2) + "\n") -PY + HEADROOM_ENABLED=true + bashio::log.info "headroom $(headroom --version 2> /dev/null || true) available; registering the headroom MCP server" else bashio::log.warning "headroom is not available" fi -elif [ -f "$CLAUDE_DESKTOP_CONFIG" ]; then - bashio::log.info "Removing the headroom MCP server from Claude Desktop" - CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" python3 - <<'PY' || bashio::log.warning "Unable to remove the headroom MCP server automatically" +fi + +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" + else + bashio::log.warning "tokensave is not available" + fi +fi + +HA_MCP_ENABLED=false +HA_MCP_URL="" +HA_MCP_TOKEN="" +if bashio::config.true 'enable_ha_mcp'; then + HA_MCP_URL="$(bashio::config 'ha_mcp_url' 'http://homeassistant:8123/mcp_server/sse')" + if bashio::config.has_value 'ha_mcp_token'; then + HA_MCP_TOKEN="$(bashio::config 'ha_mcp_token')" + fi + if [ -z "$HA_MCP_TOKEN" ]; then + bashio::log.warning "enable_ha_mcp is on but ha_mcp_token is empty; set a Home Assistant long-lived access token (Profile -> Security) and enable the 'Model Context Protocol Server' integration" + elif ! command -v mcp-proxy &> /dev/null; then + bashio::log.warning "mcp-proxy is not available; cannot register the Home Assistant MCP server" + else + HA_MCP_ENABLED=true + bashio::log.info "Registering the Home Assistant MCP server (${HA_MCP_URL})" + fi +fi + +HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \ + TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="/usr/local/bin/tokensave" \ + HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \ + MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \ + CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \ + python3 - <<'PY' || bashio::log.warning "Unable to update the MCP server registrations automatically" import json import os from pathlib import Path -path = Path(os.environ["CLAUDE_DESKTOP_CONFIG"]) -data = json.loads(path.read_text()) -if isinstance(data, dict): +MANAGED_COMMANDS = { + "headroom": {os.environ["HEADROOM_BIN"], "headroom"}, + "tokensave": {os.environ["TOKENSAVE_BIN"], "tokensave"}, + "homeassistant": {os.environ["MCP_PROXY_BIN"], "mcp-proxy"}, +} + +desired = {} +if os.environ["HEADROOM_ENABLED"] == "true": + desired["headroom"] = {"command": os.environ["HEADROOM_BIN"], "args": ["mcp", "serve"]} +if os.environ["TOKENSAVE_ENABLED"] == "true": + desired["tokensave"] = {"command": os.environ["TOKENSAVE_BIN"], "args": ["serve"]} +if os.environ["HA_MCP_ENABLED"] == "true": + desired["homeassistant"] = { + "command": os.environ["MCP_PROXY_BIN"], + "args": [os.environ["HA_MCP_URL"]], + "env": {"API_ACCESS_TOKEN": os.environ["HA_MCP_TOKEN"]}, + } + +def is_managed(name, entry): + return isinstance(entry, dict) and entry.get("command") in MANAGED_COMMANDS[name] + +for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_CONFIG", True)): + path = Path(os.environ[config_var]) + try: + data = json.loads(path.read_text()) if path.exists() else {} + if not isinstance(data, dict): + data = {} + except Exception: + if path.exists(): + path.rename(path.with_suffix(path.suffix + ".bak")) + data = {} servers = data.get("mcpServers") - if isinstance(servers, dict) and servers.pop("headroom", None) is not None: - if not servers: - data.pop("mcpServers", None) - path.write_text(json.dumps(data, indent=2) + "\n") + if not isinstance(servers, dict): + servers = {} + changed = False + for name in MANAGED_COMMANDS: + existing = servers.get(name) + if name in desired: + entry = dict(desired[name]) + if stdio_type: + entry["type"] = "stdio" + if existing is None or is_managed(name, existing): + if existing != entry: + servers[name] = entry + changed = True + elif existing is not None and is_managed(name, existing): + del servers[name] + changed = True + if not changed: + continue + if servers: + data["mcpServers"] = servers + else: + data.pop("mcpServers", None) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=2) + "\n") PY -fi # 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, @@ -216,9 +282,9 @@ else find "$HOME/.claude" -maxdepth 4 -iname '*caveman*' -exec rm -rf {} + 2> /dev/null || true fi -# Startup configuration runs as root, while Claude Desktop and the web terminal run as abc. -# Return managed persistent files to the configured runtime UID/GID after all writes complete. -for managed_path in "$HOME/.claude" "$HOME/.config/Claude"; do +# Startup configuration runs as root, while Claude Desktop runs as abc. Return managed +# persistent files to the configured runtime UID/GID after all writes complete. +for managed_path in "$HOME/.claude" "$HOME/.claude.json" "$HOME/.config/Claude"; do if [ -e "$managed_path" ]; then chown -R -- "${PUID}:${PGID}" "$managed_path" || bashio::log.warning "Unable to set ownership on $managed_path" fi diff --git a/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/dependencies.d/init-services b/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/dependencies.d/init-services deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/run b/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/run deleted file mode 100644 index afd56cb69a..0000000000 --- a/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/run +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/with-contenv bashio -# shellcheck shell=bash -set -e -set -o pipefail - -declare port=7681 -declare username -declare password="" -declare workspace -declare canonical_workspace - -export PATH="${HOME:-/data/data}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}" - -if bashio::config.has_value 'enable_terminal' && ! bashio::config.true 'enable_terminal'; then - bashio::log.info "svc-claude-terminal: terminal disabled; idling" - exec sleep infinity -fi - -if [ -z "${HOME:-}" ]; then - bashio::log.error "svc-claude-terminal: HOME is not initialized; idling" - exec sleep infinity -fi - -if ! command -v ttyd >/dev/null 2>&1 || ! command -v tmux >/dev/null 2>&1 || ! command -v claude >/dev/null 2>&1; then - bashio::log.error "svc-claude-terminal: ttyd, tmux, or Claude Code is missing; idling" - exec sleep infinity -fi - -username="claude" -if bashio::config.has_value 'terminal_username'; then - username="$(bashio::config 'terminal_username')" -fi - -if bashio::config.has_value 'terminal_password'; then - password="$(bashio::config 'terminal_password')" -elif bashio::config.has_value 'PASSWORD'; then - bashio::log.warning "svc-claude-terminal: using PASSWORD as fallback for terminal authentication; prefer a unique terminal_password" - password="$(bashio::config 'PASSWORD')" -fi - -if [ -z "$password" ]; then - bashio::log.warning "svc-claude-terminal: set terminal_password (or PASSWORD) before mapping port ${port}; terminal will remain disabled" - exec sleep infinity -fi - -workspace="${HOME}/workspace" -if bashio::config.has_value 'terminal_workspace'; then - workspace="$(bashio::config 'terminal_workspace')" -fi - -if [[ "$workspace" != /* ]]; then - bashio::log.error "svc-claude-terminal: terminal_workspace must be an absolute path; idling" - exec sleep infinity -fi - -if [ -L "$workspace" ]; then - bashio::log.error "svc-claude-terminal: terminal_workspace must not be a symbolic link; idling" - exec sleep infinity -fi - -if ! canonical_workspace="$(realpath -m -- "$workspace")"; then - bashio::log.error "svc-claude-terminal: unable to resolve terminal_workspace '$workspace'; idling" - exec sleep infinity -fi -workspace="$canonical_workspace" - -case "$workspace" in - "$HOME" | "$HOME"/* | /share/* | /media/* | /mnt/* | /data/* | /config/*) - ;; - *) - bashio::log.error "svc-claude-terminal: terminal_workspace must be the configured data_location or a subdirectory of /share, /media, /mnt, /data, or /config; idling" - exec sleep infinity - ;; -esac - -if [ ! -e "$workspace" ]; then - if ! install -d -m 0750 -o abc -g abc -- "$workspace"; then - bashio::log.error "svc-claude-terminal: failed to create workspace '$workspace'; idling" - exec sleep infinity - fi -elif [ ! -d "$workspace" ]; then - bashio::log.error "svc-claude-terminal: terminal_workspace '$workspace' is not a directory; idling" - exec sleep infinity -fi - -if ! s6-setuidgid abc test -r "$workspace" || - ! s6-setuidgid abc test -w "$workspace" || - ! s6-setuidgid abc test -x "$workspace"; then - bashio::log.error "svc-claude-terminal: workspace '$workspace' must be readable, writable, and searchable by user abc; idling" - exec sleep infinity -fi - -export CLAUDE_TERMINAL_WORKSPACE="$workspace" - -bashio::log.info "svc-claude-terminal: starting authenticated ttyd terminal on port ${port}; workspace=${workspace}" -exec s6-setuidgid abc ttyd \ - -p "$port" \ - -W \ - -O \ - -c "${username}:${password}" \ - /usr/local/bin/claude-terminal-shell diff --git a/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/type b/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/type deleted file mode 100644 index 5883cff0cd..0000000000 --- a/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/type +++ /dev/null @@ -1 +0,0 @@ -longrun 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 fa912f0dbd..475c5a47cc 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,7 +1,9 @@ #!/usr/bin/with-contenv bashio # Headroom optimization proxy — local backend for Claude Desktop MCP and Claude Code. declare port=8787 -declare host=127.0.0.1 +# 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 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}" diff --git a/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-claude-terminal b/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-claude-terminal deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/claude_desktop/rootfs/usr/local/bin/claude-direct b/claude_desktop/rootfs/usr/local/bin/claude-direct deleted file mode 100644 index 9b16e20c3f..0000000000 --- a/claude_desktop/rootfs/usr/local/bin/claude-direct +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -exec claude "$@" diff --git a/claude_desktop/rootfs/usr/local/bin/claude-headroom b/claude_desktop/rootfs/usr/local/bin/claude-headroom deleted file mode 100644 index 4c8fa247dd..0000000000 --- a/claude_desktop/rootfs/usr/local/bin/claude-headroom +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if ! command -v claude >/dev/null 2>&1; then - echo "Claude Code is not installed or is not on PATH." >&2 - exit 127 -fi - -if ! command -v headroom >/dev/null 2>&1; then - echo "Headroom is unavailable; start Claude Code directly with claude-direct." >&2 - exit 127 -fi - -if ! curl -fsS --max-time 3 "http://127.0.0.1:8787/readyz" >/dev/null; then - echo "The supervised Headroom proxy is not ready on 127.0.0.1:8787. Ensure install_headroom is enabled and check the add-on log." >&2 - exit 1 -fi - -# Reuse the s6-supervised proxy instead of starting a competing proxy. RTK is already managed -# through the persistent Claude Code PreToolUse hook, so Headroom must not reinstall it. -exec headroom wrap claude --port 8787 --no-proxy --no-rtk -- "$@" diff --git a/claude_desktop/rootfs/usr/local/bin/claude-terminal-shell b/claude_desktop/rootfs/usr/local/bin/claude-terminal-shell deleted file mode 100644 index daf449397e..0000000000 --- a/claude_desktop/rootfs/usr/local/bin/claude-terminal-shell +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [ -z "${HOME:-}" ]; then - echo "Claude terminal: HOME is not initialized." >&2 - exit 1 -fi - -export SHELL="/bin/bash" -export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}" - -workspace="${CLAUDE_TERMINAL_WORKSPACE:-${HOME}/workspace}" -session_name="${CLAUDE_TMUX_SESSION:-claude}" - -if [ ! -d "$workspace" ]; then - echo "Claude terminal: workspace does not exist: $workspace" >&2 - exit 1 -fi - -cd -- "$workspace" - -# Reattach every browser connection to the same terminal session. Closing the browser detaches -# the client but leaves Claude Code and other commands running inside tmux. -exec tmux new-session -A -s "$session_name" -c "$workspace"