mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-10-04 23:41:35 +02:00
Compare commits
43 Commits
ab02e92ad1
...
eada7a0ba0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eada7a0ba0 | ||
|
|
e7921b822e | ||
|
|
aa1486c1d8 | ||
|
|
b4008c4db9 | ||
|
|
fa2328f741 | ||
|
|
e8bb55682b | ||
|
|
7fdf95940b | ||
|
|
583c5e655a | ||
|
|
b56be1f57d | ||
|
|
50150a4775 | ||
|
|
6814088369 | ||
|
|
2951b04f57 | ||
|
|
245c52fcf2 | ||
|
|
2b6c9eace7 | ||
|
|
fd268d0e6d | ||
|
|
5792d84336 | ||
|
|
bf97abdb4f | ||
|
|
7de4789196 | ||
|
|
34e8a75bf8 | ||
|
|
652be8b13c | ||
|
|
78f5289518 | ||
|
|
12384ee606 | ||
|
|
df0dacf6b9 | ||
|
|
2cbf2a6f08 | ||
|
|
adcd892e62 | ||
|
|
980be49d9b | ||
|
|
9cff4f83b9 | ||
|
|
688d1cbdcf | ||
|
|
02bbfa86c3 | ||
|
|
a5916d9236 | ||
|
|
371c206fc0 | ||
|
|
75ce8e94e4 | ||
|
|
0b7b0ac1df | ||
|
|
2f4ab956db | ||
|
|
08a7bd35cb | ||
|
|
a313475d92 | ||
|
|
087d23eeaf | ||
|
|
a9a6b22a62 | ||
|
|
b16305a9e6 | ||
|
|
cc427f0c20 | ||
|
|
68120a84da | ||
|
|
776d063161 | ||
|
|
34135ba471 |
3
.github/workflows/onpush_builder.yaml
vendored
3
.github/workflows/onpush_builder.yaml
vendored
@@ -300,7 +300,7 @@ jobs:
|
||||
|
||||
- name: Build ${{ matrix.addon }} add-on
|
||||
if: steps.info.outputs.build_arch == 'true' && steps.info.outputs.has_dockerfile == 'true'
|
||||
uses: home-assistant/builder/actions/build-image@2026.03.2
|
||||
uses: home-assistant/builder/actions/build-image@2026.06.0
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
cache-gha: "false"
|
||||
@@ -433,4 +433,3 @@ jobs:
|
||||
done
|
||||
|
||||
git push origin HEAD:master
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
## source-20260714 (14-07-2026)
|
||||
- Minor bugs fixed
|
||||
## source-20260709 (09-07-2026)
|
||||
- Minor bugs fixed
|
||||
## source-20260708-4 (08-07-2026)
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
## 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://<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`, 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.
|
||||
|
||||
## 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}
|
||||
|
||||
##################
|
||||
@@ -61,17 +69,19 @@ 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 {} \; && \
|
||||
chmod +x /usr/local/bin/claude-direct /usr/local/bin/claude-headroom /usr/local/bin/claude-terminal-shell
|
||||
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
|
||||
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 +92,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. 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
|
||||
|
||||
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 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
|
||||
|
||||
@@ -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).
|
||||
|
||||
---
|
||||
|
||||
@@ -94,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).
|
||||
|
||||
---
|
||||
|
||||
@@ -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/api/mcp
|
||||
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.18"
|
||||
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,135 @@ 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/api/mcp')"
|
||||
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="$(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" \
|
||||
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_BASENAMES = {
|
||||
"headroom": "headroom",
|
||||
"tokensave": "tokensave",
|
||||
"homeassistant": "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":
|
||||
# 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": ["--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):
|
||||
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])
|
||||
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_BASENAMES:
|
||||
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")
|
||||
# The Home Assistant long-lived access token is stored here in clear text.
|
||||
path.chmod(0o600)
|
||||
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 +297,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"
|
||||
@@ -1,3 +1,24 @@
|
||||
## 8.19.18-3 (14-07-2026)
|
||||
- Force a fresh image pull for users left on a stale cached image (some upgrades kept running the old Elasticsearch 7.17.9 image, failing with `mv: cannot move '/data/config' ... Permission denied` and `AccessDeniedException[/usr/share/elasticsearch/data/nodes/0]`). Fully stop and update the add-on so Home Assistant pulls this build.
|
||||
- Replaced the cryptic `Permission denied` failure with a clear message when the add-on is not running as root (the state that caused the failure above).
|
||||
|
||||
## 8.19.18-2 (14-07-2026)
|
||||
- Minor bugs fixed
|
||||
## 8.19.18 (2026-07-14)
|
||||
|
||||
- Upgrade to Elasticsearch 8.19.18 (#2849). Note: despite the previous add-on version reading `8.14.3`, the shipped image was still Elasticsearch 7.17.9 — the Dockerfile upstream version was never bumped. This release actually delivers 8.x, making the add-on compatible with the `homeassistant-elasticsearch` integration (requires 8.14+).
|
||||
- Automatic data migration: existing 7.17 data is upgraded in place by Elasticsearch on first start (one-way; can take a while on large datasets). A migration guard aborts with a clear message on unsupported paths (downgrades, or data more than one major version old). Take a Home Assistant backup before updating.
|
||||
- The previous bundled config directory is archived to `/data/config.bak-<old-version>` during major upgrades; re-apply custom settings to the new config if needed.
|
||||
- Security (`xpack.security.enabled`) defaults to `false` to preserve the previous plain-HTTP behavior. Override by adding `ES_SETTING_XPACK_SECURITY_ENABLED` (or any `ES_SETTING_XPACK_SECURITY_*` variable) in the add-on's `env_vars` option.
|
||||
- Fixed the `env_vars` add-on option, which previously had no effect: variables are now exported before Elasticsearch starts.
|
||||
- Removed the `ingest-attachment` plugin install: it is a bundled module since Elasticsearch 8.0.
|
||||
- Startup persistence logic rewritten as a proper init script (`/usr/local/bin/addon-init.sh`) instead of line-number-based entrypoint patching.
|
||||
- Added `updater.json` so upstream 8.19.x releases are tracked automatically (pinned to the 8.19 line: 9.x cannot read indices created in 7.x).
|
||||
- The upstream 8.x image ends the build as a non-root user with a read-only entrypoint; the Dockerfile now switches to root for the build steps that patch/install into it. The container also starts as root (unchanged from 7.17.9) so `addon-init.sh` can chown/move pre-existing `/data` content that may be owned by root from earlier installs; unlike 7.17.9's own entrypoint, the upstream 8.x entrypoint no longer drops privileges before starting Elasticsearch (which refuses to run as root), so `addon-init.sh` now does that itself via `chroot --userspec=1000:0` once its root-only work is done.
|
||||
- `env_vars` names starting with a digit are now rejected before export instead of crashing the entrypoint.
|
||||
- Fixed a startup failure (`mv: cannot move '/data/config' ... Permission denied`) on upgrade from an existing 7.17.9 install, caused by an earlier fix in this same release that switched the runtime user to non-root before this fix was in place.
|
||||
- Fixed a second regression from that same fix: without a privilege drop before starting Elasticsearch, both fresh installs and upgrades would fail Elasticsearch's own root-check ("can not run elasticsearch as root").
|
||||
|
||||
## 8.14.3-3 (2026-06-19)
|
||||
- Fix startup failing with `chroot: cannot change root directory` by allowing `capability sys_chroot` in the AppArmor profile (#2709)
|
||||
- Fix AppArmor profile name (was `inadyn_addon`, colliding with several other add-ons); renamed to `elasticsearch_addon`
|
||||
|
||||
@@ -14,9 +14,14 @@
|
||||
# 1 Build Image #
|
||||
#################
|
||||
|
||||
ARG BUILD_UPSTREAM="7.17.9"
|
||||
ARG BUILD_UPSTREAM="8.19.18"
|
||||
FROM elasticsearch:$BUILD_UPSTREAM
|
||||
|
||||
# The base image ends as USER 1000:0 with a root-owned, read-only (0555)
|
||||
# entrypoint; switch back to root for the remaining build steps (entrypoint
|
||||
# patch, package install, chmod), then restore the Elasticsearch user below
|
||||
USER root
|
||||
|
||||
##################
|
||||
# 2 Modify Image #
|
||||
##################
|
||||
@@ -26,20 +31,15 @@ ENV S6_CMD_WAIT_FOR_SERVICES=1 \
|
||||
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
|
||||
S6_SERVICES_GRACETIME=0
|
||||
|
||||
# Expose the upstream version to the add-on init script (migration guard)
|
||||
ARG BUILD_UPSTREAM
|
||||
ENV UPSTREAM_VERSION="$BUILD_UPSTREAM"
|
||||
|
||||
# Data persistence
|
||||
# hadolint ignore=SC2016
|
||||
RUN sed -i '5a echo "Data location moved. Please wait while elasticsearch starts..."' /usr/local/bin/docker-entrypoint.sh \
|
||||
&& sed -i '5a chown -R $(id -u):$(id -g) $HOME' /usr/local/bin/docker-entrypoint.sh \
|
||||
&& sed -i '5a done' /usr/local/bin/docker-entrypoint.sh \
|
||||
&& sed -i '5a ln -s $NEWHOME/$file /usr/share/elasticsearch || true' /usr/local/bin/docker-entrypoint.sh \
|
||||
&& sed -i '5a rm -r /usr/share/elasticsearch/$file || true' /usr/local/bin/docker-entrypoint.sh \
|
||||
&& sed -i '5a cp -rn /usr/share/elasticsearch/$file $NEWHOME || true' /usr/local/bin/docker-entrypoint.sh \
|
||||
&& sed -i '5a for file in "data" "config"; do' /usr/local/bin/docker-entrypoint.sh \
|
||||
&& sed -i '5a mkdir -p $NEWHOME' /usr/local/bin/docker-entrypoint.sh \
|
||||
&& sed -i '5a NEWHOME="/data"' /usr/local/bin/docker-entrypoint.sh \
|
||||
# Install plugins
|
||||
&& /usr/share/elasticsearch/bin/elasticsearch-plugin install --batch ingest-attachment
|
||||
# Data persistence & migration: source the add-on init script at the top of
|
||||
# the official entrypoint (pattern-anchored; ingest-attachment is bundled
|
||||
# since ES 8.0 so no plugin install is needed anymore)
|
||||
RUN sed -i '/^set -e$/a . /usr/local/bin/addon-init.sh' /usr/local/bin/docker-entrypoint.sh \
|
||||
&& grep -q "addon-init.sh" /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
##################
|
||||
# 3 Install apps #
|
||||
@@ -62,7 +62,7 @@ COPY ha_automodules.sh /ha_automodules.sh
|
||||
RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_automodules.sh
|
||||
|
||||
# Manual apps
|
||||
ENV PACKAGES=""
|
||||
ENV PACKAGES="jq"
|
||||
|
||||
# Automatic apps & bashio
|
||||
COPY ha_autoapps.sh /ha_autoapps.sh
|
||||
@@ -140,3 +140,11 @@ HEALTHCHECK \
|
||||
--start-period=30s \
|
||||
--timeout=25s \
|
||||
CMD curl -A "HealthCheck: Docker/1.0" -s -f "http://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" &>/dev/null || exit 1
|
||||
|
||||
# Start as root: addon-init.sh needs it to chown/move pre-existing /data
|
||||
# content that may be owned by root from earlier installs. It drops to
|
||||
# uid 1000 itself (via chroot --userspec) before Elasticsearch actually
|
||||
# starts, since Elasticsearch refuses to run as root and the upstream 8.x
|
||||
# entrypoint no longer does that drop on its own (7.x's did). This matches
|
||||
# the addon's own AppArmor profile (chown, setuid, setgid, sys_chroot,
|
||||
# mount capabilities).
|
||||
|
||||
@@ -91,6 +91,22 @@ Connect other applications to Elasticsearch using:
|
||||
|
||||
Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||
Elasticsearch settings can be set through variables named `ES_SETTING_<SETTING_WITH_UNDERSCORES>`; for example `ES_SETTING_XPACK_SECURITY_ENABLED` maps to `xpack.security.enabled`.
|
||||
|
||||
### Security
|
||||
|
||||
To preserve the plain-HTTP behavior of previous versions (and compatibility with the Home Assistant Elasticsearch integration), `xpack.security.enabled` defaults to `false`. To enable Elasticsearch security, add `ES_SETTING_XPACK_SECURITY_ENABLED` with value `true` in `env_vars`.
|
||||
|
||||
## Upgrading from 7.x
|
||||
|
||||
The upgrade to Elasticsearch 8.x is automatic and **one-way**:
|
||||
|
||||
1. Take a Home Assistant backup of the add-on before updating.
|
||||
2. Update the add-on and start it. Elasticsearch upgrades the existing indices in place on first start — this can take a while on large datasets; do **not** stop the add-on during the first start.
|
||||
3. The previous bundled config directory is archived to `/data/config.bak-<old-version>`; re-apply any custom settings to the new config.
|
||||
|
||||
Downgrading afterwards is not supported by Elasticsearch — restore the backup instead.
|
||||
|
||||
## Integration with HA
|
||||
|
||||
Component : https://community.home-assistant.io/t/elasticsearch-component-publish-home-assistant-events-to-elasticsearch/66877
|
||||
|
||||
@@ -90,4 +90,4 @@ slug: elasticsearch
|
||||
startup: services
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/elasticsearch
|
||||
version: 8.14.3-3
|
||||
version: 8.19.18-3
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
#!/bin/bash
|
||||
182
elasticsearch/rootfs/usr/local/bin/addon-init.sh
Executable file
182
elasticsearch/rootfs/usr/local/bin/addon-init.sh
Executable file
@@ -0,0 +1,182 @@
|
||||
#!/bin/bash
|
||||
# shellcheck shell=bash
|
||||
# Sourced by /usr/local/bin/docker-entrypoint.sh (right after "set -e"),
|
||||
# before Elasticsearch starts. The container starts as root (see
|
||||
# Dockerfile) so this script can chown/move pre-existing /data content
|
||||
# that may be owned by root from earlier installs. Elasticsearch itself
|
||||
# refuses to run as root, and unlike 7.x the upstream 8.x entrypoint no
|
||||
# longer drops privileges on its own, so this script does it at the end
|
||||
# (section 6) by re-execing the entrypoint as uid 1000. On that re-exec'd
|
||||
# pass this script just returns immediately (see the guard right below).
|
||||
#
|
||||
# Responsibilities:
|
||||
# 1. Export user env_vars from /data/options.json
|
||||
# 2. Default xpack.security.enabled=false (7.x behavior) unless user overrides
|
||||
# 3. Relocate data & config to /data for persistence (idempotent)
|
||||
# 4. Guard major-version data migrations (7.x -> 8.x is automatic)
|
||||
# 5. Record the running version once Elasticsearch is confirmed healthy
|
||||
# 6. Drop root privileges before Elasticsearch actually starts
|
||||
|
||||
if [ -n "${_ADDON_INIT_REEXEC:-}" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "-----------------------------------------------------------"
|
||||
echo " Add-on: Elasticsearch server"
|
||||
echo " Upstream version: ${UPSTREAM_VERSION:-unknown}"
|
||||
echo "-----------------------------------------------------------"
|
||||
|
||||
ES_HOME="/usr/share/elasticsearch"
|
||||
PERSISTENT_HOME="/data"
|
||||
VERSION_MARKER="$PERSISTENT_HOME/.addon-upstream-version"
|
||||
OPTIONS_JSON="/data/options.json"
|
||||
|
||||
# This first pass must be root so it can relocate and take ownership of
|
||||
# pre-existing /data content written by an earlier (root) install. If it
|
||||
# is not root (e.g. an old cached image that pinned USER 1000:0, or the
|
||||
# container being forced to another user), the moves/chowns below fail
|
||||
# with a cryptic "Permission denied"; fail loudly with the real reason.
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "FATAL: the Elasticsearch add-on must start as root (currently uid $(id -u))."
|
||||
echo "If you upgraded from an older version, the running image is likely stale - fully stop and update/reinstall the add-on so Home Assistant pulls the current image."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
############################
|
||||
# 1 Export user env_vars #
|
||||
############################
|
||||
|
||||
if [ -f "$OPTIONS_JSON" ] && command -v jq >/dev/null 2>&1; then
|
||||
while IFS= read -r pair; do
|
||||
name=$(jq -r '.name // empty' <<<"$pair")
|
||||
value=$(jq -r '.value // empty' <<<"$pair")
|
||||
if [[ $name =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
|
||||
echo "Setting env variable from options: $name"
|
||||
export "$name"="$value"
|
||||
elif [ -n "$name" ]; then
|
||||
echo "WARNING: ignoring invalid env_vars name: $name"
|
||||
fi
|
||||
done < <(jq -c '.env_vars[]?' "$OPTIONS_JSON" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
##################################
|
||||
# 2 Security default (7.x parity)#
|
||||
##################################
|
||||
|
||||
# ES 8+ enables security + TLS by default, which breaks plain-http clients
|
||||
# such as the homeassistant-elasticsearch component. Keep the previous 7.x
|
||||
# behavior unless the user explicitly configures xpack.security themselves
|
||||
# (either as a dotted setting or via the ES_SETTING_* translation).
|
||||
if ! env | grep -qiE '^(xpack\.security\.|ES_SETTING_XPACK_SECURITY_)'; then
|
||||
export ES_SETTING_XPACK_SECURITY_ENABLED=false
|
||||
echo "Security: xpack.security.enabled=false (default; override by setting ES_SETTING_XPACK_SECURITY_ENABLED in env_vars)"
|
||||
fi
|
||||
|
||||
############################
|
||||
# 3 Migration guard #
|
||||
############################
|
||||
|
||||
current_version="${UPSTREAM_VERSION:-0.0.0}"
|
||||
current_major="${current_version%%.*}"
|
||||
data_version=""
|
||||
|
||||
if [ -f "$VERSION_MARKER" ]; then
|
||||
data_version="$(head -n 1 "$VERSION_MARKER" | tr -cd '0-9.')"
|
||||
elif [ -d "$PERSISTENT_HOME/data" ] && [ -n "$(ls -A "$PERSISTENT_HOME/data" 2>/dev/null)" ]; then
|
||||
# Existing data without a marker: only 7.17.9 was ever shipped before markers
|
||||
data_version="7.17.9"
|
||||
fi
|
||||
|
||||
if [ -n "$data_version" ] && [[ $current_major =~ ^[0-9]+$ ]]; then
|
||||
data_major="${data_version%%.*}"
|
||||
if [ "$data_major" -gt "$current_major" ]; then
|
||||
echo "FATAL: existing data was written by Elasticsearch $data_version but this add-on runs $current_version."
|
||||
echo "Downgrading Elasticsearch data is not supported. Restore a Home Assistant snapshot taken with the newer version, or delete the add-on data to start fresh."
|
||||
exit 1
|
||||
elif [ "$((current_major - data_major))" -gt 1 ]; then
|
||||
echo "FATAL: existing data was written by Elasticsearch $data_version, which is more than one major version behind $current_version."
|
||||
echo "Elasticsearch can only upgrade data from the previous major version. Upgrade stepwise (e.g. $data_major.x -> $((data_major + 1)).x -> ...) or delete the add-on data to start fresh."
|
||||
exit 1
|
||||
elif [ "$data_major" -lt "$current_major" ]; then
|
||||
echo "NOTICE: one-time automatic data migration from Elasticsearch $data_version to $current_version."
|
||||
echo "NOTICE: indices are upgraded automatically on startup. This can take a while on large datasets - do NOT stop the add-on during the first start."
|
||||
# The bundled config from the old major is stale (jvm.options, log4j2,
|
||||
# security settings). Archive it so a fresh one is seeded below.
|
||||
if [ -d "$PERSISTENT_HOME/config" ] && [ ! -L "$PERSISTENT_HOME/config" ]; then
|
||||
config_backup="$PERSISTENT_HOME/config.bak-$data_version"
|
||||
if [ ! -e "$config_backup" ]; then
|
||||
if ! mv "$PERSISTENT_HOME/config" "$config_backup"; then
|
||||
echo "FATAL: could not archive the old config to $config_backup."
|
||||
echo "This add-on must run as root to migrate a previous install. Restore a Home Assistant backup and ensure the add-on is not forced to a non-root user."
|
||||
exit 1
|
||||
fi
|
||||
echo "NOTICE: previous config archived to $config_backup. Re-apply any custom settings to the new config."
|
||||
fi
|
||||
fi
|
||||
# The container config dir may still symlink to the archived config
|
||||
if [ -L "$ES_HOME/config" ]; then
|
||||
rm -f "$ES_HOME/config"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
############################
|
||||
# 4 Data persistence #
|
||||
############################
|
||||
|
||||
mkdir -p "$PERSISTENT_HOME"
|
||||
for dir in "data" "config"; do
|
||||
if [ ! -L "$ES_HOME/$dir" ]; then
|
||||
if [ -d "$ES_HOME/$dir" ]; then
|
||||
cp -rn "$ES_HOME/$dir" "$PERSISTENT_HOME" 2>/dev/null || true
|
||||
rm -rf "${ES_HOME:?}/$dir"
|
||||
fi
|
||||
mkdir -p "$PERSISTENT_HOME/$dir"
|
||||
ln -s "$PERSISTENT_HOME/$dir" "$ES_HOME/$dir"
|
||||
fi
|
||||
done
|
||||
|
||||
# Make the persisted files usable by the elasticsearch user (uid 1000),
|
||||
# which the official entrypoint drops to when started as root
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
chown -R 1000:0 "$PERSISTENT_HOME/data" "$PERSISTENT_HOME/config" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "Data location: $PERSISTENT_HOME (persistent). Please wait while elasticsearch starts..."
|
||||
|
||||
############################
|
||||
# 5 Record data version #
|
||||
############################
|
||||
|
||||
# Only record the running version once ES is confirmed healthy, so a failed
|
||||
# upgrade attempt never masks the true on-disk data lineage
|
||||
if [ "$data_version" != "$current_version" ]; then
|
||||
(
|
||||
for _ in $(seq 1 180); do
|
||||
# Check the HTTP status directly instead of curl -f: a 401 means
|
||||
# Elasticsearch is up and answering (security just requires
|
||||
# auth), so it must count as healthy too, not as a failure.
|
||||
status=$(curl -A "HealthCheck: Docker/1.0" -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:9200" 2>/dev/null || true)
|
||||
if [ "$status" = "200" ] || [ "$status" = "401" ]; then
|
||||
echo "$current_version" >"$VERSION_MARKER"
|
||||
echo "Elasticsearch $current_version started successfully; data version recorded."
|
||||
exit 0
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
) &
|
||||
fi
|
||||
|
||||
############################
|
||||
# 6 Drop privileges #
|
||||
############################
|
||||
|
||||
# Elasticsearch refuses to start as root ("can not run elasticsearch as
|
||||
# root"). 7.x's own entrypoint dropped to uid 1000 via chroot before
|
||||
# launching Elasticsearch; 8.x no longer does that, so do it here instead,
|
||||
# then let the entrypoint continue as uid 1000 (matches the sys_chroot /
|
||||
# setuid / setgid capabilities already granted in the AppArmor profile).
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
export _ADDON_INIT_REEXEC=1
|
||||
exec chroot --userspec=1000:0 / /usr/local/bin/docker-entrypoint.sh "$@"
|
||||
fi
|
||||
10
elasticsearch/updater.json
Normal file
10
elasticsearch/updater.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"github_fulltag": false,
|
||||
"github_tagfilter": "v8.19",
|
||||
"last_update": "14-07-2026",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "elasticsearch",
|
||||
"source": "github",
|
||||
"upstream_repo": "elastic/elasticsearch",
|
||||
"upstream_version": "8.19.18"
|
||||
}
|
||||
Reference in New Issue
Block a user