mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-08-17 18:37:19 +02:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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://<host>: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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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://<home-assistant-host>: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 `<data_location>/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://<home-assistant-host>: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`.
|
||||
|
||||
@@ -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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -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://<ha-ip>: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}"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
exec claude "$@"
|
||||
@@ -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 -- "$@"
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user