From e7921b822e5c604b30cf11e6c3ab458583ee7ebf Mon Sep 17 00:00:00 2001 From: alexbelgium Date: Tue, 14 Jul 2026 16:52:15 +0200 Subject: [PATCH] fix(claude_desktop): correct HA MCP endpoint, harden config perms and chmod scope Home Assistant's MCP Server integration serves stateless Streamable HTTP at /api/mcp; mcp-proxy defaults to SSE, so the previous registration (SSE at /mcp_server/sse) could never attach. Pass --transport=streamablehttp --stateless and default ha_mcp_url to /api/mcp. Match managed MCP entries by binary basename outside $HOME so a base-image path change still updates them, while user-installed binaries under $HOME remain untouched. Resolve tokensave via command -v like the others. Write Claude config files 0600 (they hold the HA long-lived token in clear text) and scope the build-time chmod +x pass to the shipped script dirs. Docs: dashboard reachability wording, stale /config/data HOME, and the custom-script filename (claude_desktop.sh, per the $slug.sh template). Co-Authored-By: Claude Sonnet 5 --- claude_desktop/CHANGELOG.md | 6 ++-- claude_desktop/Dockerfile | 6 ++-- claude_desktop/README.md | 14 ++++---- claude_desktop/SIGN_IN.md | 6 ++-- claude_desktop/config.yaml | 4 +-- .../rootfs/etc/cont-init.d/82-claude_tools.sh | 33 ++++++++++++++----- 6 files changed, 44 insertions(+), 25 deletions(-) diff --git a/claude_desktop/CHANGELOG.md b/claude_desktop/CHANGELOG.md index bac60d1043..127a260bbd 100644 --- a/claude_desktop/CHANGELOG.md +++ b/claude_desktop/CHANGELOG.md @@ -1,11 +1,13 @@ -## 1.17 (14-07-2026) +## 1.18 (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`. +- 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`, using the integration's stateless Streamable HTTP endpoint (`/api/mcp`). +- Write the Claude configuration files with `0600` permissions, since they hold the Home Assistant access token in clear text. +- Restrict the build-time `chmod +x` pass to the directories the add-on actually ships scripts in instead of traversing the whole image. - 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. diff --git a/claude_desktop/Dockerfile b/claude_desktop/Dockerfile index 1157d6d62e..e23b21540d 100644 --- a/claude_desktop/Dockerfile +++ b/claude_desktop/Dockerfile @@ -69,9 +69,11 @@ RUN curl -fsSL --retry 3 --retry-delay 2 \ # 3 Install apps # ################## -# Add rootfs +# Add rootfs. Only the directories this add-on ships scripts in are traversed, so the chmod +# cannot alter executables elsewhere in the image. COPY rootfs/ / -RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \; +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 {} \; # Uses /bin for compatibility purposes # hadolint ignore=DL4005 diff --git a/claude_desktop/README.md b/claude_desktop/README.md index bcb3064c8b..4967a19655 100644 --- a/claude_desktop/README.md +++ b/claude_desktop/README.md @@ -96,10 +96,10 @@ The Headroom dashboard is available at: http://:8787/dashboard ``` -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. +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. ## Home Assistant MCP bridge @@ -111,9 +111,9 @@ To let Claude query and control Home Assistant: 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. +The add-on bridges Claude to the integration's stateless Streamable HTTP +endpoint (`/api/mcp`) with `mcp-proxy`. Override `ha_mcp_url` only if your Home +Assistant instance is not reachable as `homeassistant:8123` from add-ons. ## Custom scripts diff --git a/claude_desktop/SIGN_IN.md b/claude_desktop/SIGN_IN.md index 3e7c007565..78b717e8c1 100644 --- a/claude_desktop/SIGN_IN.md +++ b/claude_desktop/SIGN_IN.md @@ -54,7 +54,7 @@ magic link into the in-session Chromium (not a phone). - Add-on Configuration → `additional_apps: chromium`, restart (installed by `rootfs/etc/cont-init.d/80-configuration.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 + `/addon_configs/db21ed7f_claude-desktop/claude_desktop.sh` (the image ships no standalone terminal). --- @@ -98,14 +98,14 @@ Claude Desktop uses. No extra `dbus-launch` is needed. then exposes the Secret Service and exports `GNOME_KEYRING_CONTROL`/`SSH_AUTH_SOCK`. - `--password-store=gnome-libsecret` forces Electron to use the libsecret backend instead of falling back to plaintext. -3. Persistence: the keyring DB lives in `$HOME/.local/share/keyrings/` and `HOME=/config/data` +3. Persistence: the keyring DB lives in `$HOME/.local/share/keyrings/` and `HOME=/data/data` (persistent add-on storage), so the empty-password login keyring survives restarts and is re-unlocked automatically each boot by the same `autostart` line — the sign-in then sticks. ### User-side workaround (no rebuild) - Add-on Configuration → `additional_apps: gnome-keyring, libsecret-1-0, dbus-x11`, restart. - Add the keyring-start lines above to the custom script - `/addon_configs/db21ed7f_claude-desktop/claude-desktop.sh`, and relaunch Claude Desktop + `/addon_configs/db21ed7f_claude-desktop/claude_desktop.sh`, and relaunch Claude Desktop with `--password-store=gnome-libsecret` (e.g. edit the in-session openbox autostart). --- diff --git a/claude_desktop/config.yaml b/claude_desktop/config.yaml index 86cea61bdf..c6e563c9c0 100644 --- a/claude_desktop/config.yaml +++ b/claude_desktop/config.yaml @@ -42,7 +42,7 @@ options: auto_update: true github_email: "" enable_ha_mcp: false - ha_mcp_url: http://homeassistant:8123/mcp_server/sse + ha_mcp_url: http://homeassistant:8123/api/mcp ha_mcp_token: "" github_token: "" github_username: "" @@ -92,5 +92,5 @@ slug: claude_desktop tmpfs: true udev: true url: https://github.com/alexbelgium/hassio-addons -version: "1.17" +version: "1.18" video: true 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 3246dc2bae..17f6a2084a 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 @@ -48,7 +48,7 @@ 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')" + HA_MCP_URL="$(bashio::config 'ha_mcp_url' 'http://homeassistant:8123/api/mcp')" if bashio::config.has_value 'ha_mcp_token'; then HA_MCP_TOKEN="$(bashio::config 'ha_mcp_token')" fi @@ -63,7 +63,7 @@ if bashio::config.true 'enable_ha_mcp'; then fi HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \ - TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="/usr/local/bin/tokensave" \ + TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo 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" \ @@ -72,10 +72,10 @@ import json import os from pathlib import Path -MANAGED_COMMANDS = { - "headroom": {os.environ["HEADROOM_BIN"], "headroom"}, - "tokensave": {os.environ["TOKENSAVE_BIN"], "tokensave"}, - "homeassistant": {os.environ["MCP_PROXY_BIN"], "mcp-proxy"}, +MANAGED_BASENAMES = { + "headroom": "headroom", + "tokensave": "tokensave", + "homeassistant": "mcp-proxy", } desired = {} @@ -84,14 +84,27 @@ if os.environ["HEADROOM_ENABLED"] == "true": if os.environ["TOKENSAVE_ENABLED"] == "true": desired["tokensave"] = {"command": os.environ["TOKENSAVE_BIN"], "args": ["serve"]} if os.environ["HA_MCP_ENABLED"] == "true": + # Home Assistant's MCP Server integration speaks stateless Streamable HTTP on /api/mcp; + # mcp-proxy defaults to SSE, so the transport flags are required. desired["homeassistant"] = { "command": os.environ["MCP_PROXY_BIN"], - "args": [os.environ["HA_MCP_URL"]], + "args": ["--transport=streamablehttp", "--stateless", os.environ["HA_MCP_URL"]], "env": {"API_ACCESS_TOKEN": os.environ["HA_MCP_TOKEN"]}, } +# An entry is add-on-managed when its command is one of our binaries living outside the +# persistent home. Matching on the basename (rather than the exact path recorded at write +# time) keeps entries updatable when a base-image upgrade moves the binary, while commands +# under $HOME stay untouched because those are user-installed. +HOME_PREFIX = os.path.expanduser("~") + os.sep + def is_managed(name, entry): - return isinstance(entry, dict) and entry.get("command") in MANAGED_COMMANDS[name] + if not isinstance(entry, dict): + return False + command = entry.get("command") + if not isinstance(command, str) or command.startswith(HOME_PREFIX): + 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]) @@ -107,7 +120,7 @@ for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_C if not isinstance(servers, dict): servers = {} changed = False - for name in MANAGED_COMMANDS: + for name in MANAGED_BASENAMES: existing = servers.get(name) if name in desired: entry = dict(desired[name]) @@ -128,6 +141,8 @@ for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_C data.pop("mcpServers", None) path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, indent=2) + "\n") + # The Home Assistant long-lived access token is stored here in clear text. + path.chmod(0o600) PY # Guide Claude to actually use the headroom compression tools so the MCP integration produces