Compare commits

...

43 Commits

Author SHA1 Message Date
Alexandre
eada7a0ba0 Merge pull request #2860 from alexbelgium/feat/claude-desktop-only-v2
fix(claude_desktop): correct HA MCP endpoint, harden config perms and chmod scope
2026-07-14 16:52:58 +02:00
alexbelgium
e7921b822e fix(claude_desktop): correct HA MCP endpoint, harden config perms and chmod scope
Home Assistant's MCP Server integration serves stateless Streamable HTTP at
/api/mcp; mcp-proxy defaults to SSE, so the previous registration (SSE at
/mcp_server/sse) could never attach. Pass --transport=streamablehttp
--stateless and default ha_mcp_url to /api/mcp.

Match managed MCP entries by binary basename outside $HOME so a base-image
path change still updates them, while user-installed binaries under $HOME
remain untouched. Resolve tokensave via command -v like the others.

Write Claude config files 0600 (they hold the HA long-lived token in clear
text) and scope the build-time chmod +x pass to the shipped script dirs.

Docs: dashboard reachability wording, stale /config/data HOME, and the
custom-script filename (claude_desktop.sh, per the $slug.sh template).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 16:52:15 +02:00
Alexandre
aa1486c1d8 nobuild 2026-07-14 16:39:19 +02:00
GitHub Actions
b4008c4db9 Revert "update"
This reverts commit fa2328f741.
2026-07-14 14:37:19 +00:00
Alexandre
fa2328f741 update 2026-07-14 16:36:25 +02:00
Alexandre
e8bb55682b Merge pull request #2859 from alexbelgium/bump-builder-2026.06.0
ci: bump builder build-image action to 2026.06.0
2026-07-14 16:35:01 +02:00
Alexandre
7fdf95940b Merge pull request #2858 from alexbelgium/feat/claude-desktop-only
feat(claude_desktop): desktop-only architecture, fix dashboard + dispatch
2026-07-14 16:33:19 +02:00
alexbelgium
583c5e655a ci: bump home-assistant/builder build-image action to 2026.06.0
Legacy monolithic builder was removed upstream in 2026.06.0; this repo
already uses the modular build-image action, so only the pin moves
(2026.03.2 -> 2026.06.0). Action inputs/outputs unchanged upstream —
drop-in compatible. Also strips trailing whitespace at EOF (yamllint).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 16:31:24 +02:00
alexbelgium
b56be1f57d 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>
2026-07-14 16:27:52 +02:00
github-actions
50150a4775 GitHub bot: changelog [nobuild] 2026-07-14 14:26:50 +00:00
github-actions
6814088369 GitHub bot: sanitize (spaces + LF endings) & chmod [nobuild] 2026-07-14 14:19:56 +00:00
Alexandre
2951b04f57 Update config.yaml 2026-07-14 16:18:58 +02:00
Alexandre
245c52fcf2 Merge pull request #2857 from alexbelgium/agent/add-chatgpt-codex-addon
Add ChatGPT Codex add-on with Headroom and RTK
2026-07-14 16:18:22 +02:00
Alexandre
2b6c9eace7 Release latest-version build policy 2026-07-14 16:04:13 +02:00
Alexandre
fd268d0e6d Normalize latest-version documentation 2026-07-14 16:01:29 +02:00
Alexandre
5792d84336 Keep updater metadata separate from build resolution 2026-07-14 16:00:57 +02:00
Alexandre
bf97abdb4f Document latest-version build policy 2026-07-14 16:00:44 +02:00
Alexandre
7de4789196 Document unpinned tool installation 2026-07-14 15:59:05 +02:00
Alexandre
34e8a75bf8 Install latest tool releases at build time 2026-07-14 15:58:55 +02:00
Alexandre
652be8b13c Pin Codex and Headroom releases 2026-07-14 15:03:34 +02:00
Alexandre
78f5289518 Fix Codex add-on lint metadata 2026-07-14 14:59:11 +02:00
Alexandre
12384ee606 Follow custom data location for Codex workspace 2026-07-14 14:55:02 +02:00
Alexandre
df0dacf6b9 Restrict Codex ingress to administrators 2026-07-14 14:53:30 +02:00
Alexandre
2cbf2a6f08 Add ChatGPT Codex add-on with Headroom and RTK 2026-07-14 14:51:06 +02:00
Alexandre
adcd892e62 Merge pull request #2856 from alexbelgium/fix/elasticsearch-force-rebuild
fix(elasticsearch): force fresh image pull for users stuck on a stale 7.17.9 image
2026-07-14 14:34:59 +02:00
Alexandre
980be49d9b Merge branch 'master' into fix/elasticsearch-force-rebuild 2026-07-14 14:34:28 +02:00
github-actions
9cff4f83b9 GitHub bot: changelog [nobuild] 2026-07-14 11:42:46 +00:00
Alexandre
688d1cbdcf Update config.yaml 2026-07-14 13:40:23 +02:00
alexbelgium
02bbfa86c3 fix(elasticsearch): force fresh image pull, clarify non-root failure
The published 8.19.18 images are correct (verified: real ES 8.19.18,
run as root, migration + privilege-drop in place). But some upgrades
were left running a stale cached Elasticsearch 7.17.9 image that starts
as uid 1000, producing the reported "mv: cannot move '/data/config' ...
Permission denied" and "AccessDeniedException[.../data/nodes/0]".

- Bump version to 8.19.18-3 to force Home Assistant / Docker to pull a
  fresh image tag instead of reusing the cached one.
- Add an explicit root check on the first init pass (before any move or
  chown) so a non-root start fails with a clear, actionable message
  instead of the cryptic permission error, and wrap the config-archive
  mv with the same clear failure. The re-exec'd uid-1000 pass returns
  before this check, so the privilege drop still works.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:35:03 +02:00
Alexandre
a5916d9236 nobuild 2026-07-14 13:06:55 +02:00
Alexandre
371c206fc0 Update Elasticsearch version to 8.19.18 2026-07-14 13:06:17 +02:00
github-actions
75ce8e94e4 GitHub bot: changelog [nobuild] 2026-07-14 10:47:03 +00:00
Alexandre
0b7b0ac1df Update config.yaml 2026-07-14 12:43:40 +02:00
Alexandre
2f4ab956db Update config.yaml 2026-07-14 12:43:18 +02:00
Alexandre
08a7bd35cb Merge pull request #2854 from alexbelgium/fix/elasticsearch-runtime-root-permission
fix(elasticsearch): stay root at runtime, fix upgrade permission failure
2026-07-14 12:41:49 +02:00
alexbelgium
a313475d92 fix(elasticsearch): drop to uid 1000 before starting Elasticsearch
Elasticsearch refuses to bootstrap as root ("can not run elasticsearch
as root"). The previous fix in this PR kept the container root at
runtime to fix the /data permission failure, but never dropped
privileges again afterward — unlike 7.17.9, whose own entrypoint used
`chroot --userspec=1000:0` before launching Elasticsearch, the upstream
8.x entrypoint no longer does that. So every start, fresh or upgrade,
would fail once addon-init.sh's setup finished.

Fix: after addon-init.sh completes its root-only work (migration guard,
data/config relocation, chown), it re-execs the entrypoint itself as
uid 1000 via `chroot --userspec=1000:0 / ...` — the same mechanism
7.17.9 used, and exactly what the add-on's AppArmor profile already
grants (sys_chroot, setuid, setgid). On the re-exec'd pass the script
returns immediately (guarded by an exported sentinel) so none of the
setup work repeats; exported env vars (env_vars, the security default)
survive the exec normally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 12:33:26 +02:00
alexbelgium
087d23eeaf fix(elasticsearch): stay root at runtime, fix upgrade permission failure
Reported: on upgrade from an existing 7.17.9 install, the add-on failed
to start with "mv: cannot move '/data/config' to
'/data/config.bak-7.17.9': Permission denied".

Root cause: a previous fix in this same release restored `USER 1000:0`
at the end of the Dockerfile to match the upstream base image's own
final USER directive. But the upstream 8.19 entrypoint no longer drops
privileges itself (confirmed: it execs elasticsearch directly, no
gosu/chroot dance), and existing installs have /data owned by root
(7.17.9's default image variant runs fully as root). A non-root
container can never chown or move that data.

Revert to root at runtime, matching how this add-on always ran and
matching its own AppArmor profile (chown, setuid, setgid, sys_chroot,
mount capabilities — all meaningless for a non-root process anyway).
Root stays required for the build-time entrypoint patch too, unchanged
from the prior fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 12:12:16 +02:00
github-actions
a9a6b22a62 GitHub bot: sanitize (spaces + LF endings) & chmod [nobuild] 2026-07-14 09:43:11 +00:00
Alexandre
b16305a9e6 Merge pull request #2853 from alexbelgium/fix/elasticsearch-8x-migration
fix(elasticsearch): upgrade to 8.19.18 with automatic data migration
2026-07-14 11:42:25 +02:00
alexbelgium
cc427f0c20 fix(elasticsearch): treat HTTP 401 as healthy in the migration marker check
curl -f treated any 4xx as failure, including 401. Users who enable
xpack.security (a supported override via ES_SETTING_XPACK_SECURITY_ENABLED)
got 401 on the unauthenticated healthcheck request, so the version marker
was never written and every restart re-logged the one-time migration
notice. Read the HTTP status directly and accept 200 or 401.

Reviewed and skipped: the cp -rn merge-into-existing-directory concern —
verified empirically (both locally and against the image's Debian/GNU
coreutils base) that GNU cp merges correctly into a pre-existing
same-named destination without nesting; the existing test suite already
exercises this exact path (legacy 7.x data preserved during migration).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 11:26:43 +02:00
alexbelgium
68120a84da fix(elasticsearch): address review — build user, env_vars validation
- The 8.19.18 base image ends the build as USER 1000:0 with a
  root-owned, read-only (0555) entrypoint, so the sed patch and later
  chmod/package-install steps failed. Switch to root for the build and
  restore the Elasticsearch user before runtime.
- Tighten the env_vars name check to require a leading letter/underscore
  (shell identifier rules) instead of allowing a leading digit, which
  made `export "$name"=...` fail and abort startup under `set -e`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 11:18:49 +02:00
alexbelgium
776d063161 fix(elasticsearch): upgrade to 8.19.18 with automatic data migration
The add-on reported version 8.14.3 but the shipped image was still
Elasticsearch 7.17.9 — the Dockerfile BUILD_UPSTREAM was never bumped,
and the builder uses that ARG. The homeassistant-elasticsearch
integration requires 8.14+, so configuration failed (#2849).

- Upgrade to Elasticsearch 8.19.18 (latest 8.x; 9.x cannot read indices
  created in 7.x)
- Add automatic 7.x -> 8.x data migration with a guard that aborts on
  unsupported paths (downgrade, or data more than one major behind).
  The version marker is written only after ES answers on 9200, so a
  failed upgrade never masks the true on-disk data lineage
- Default xpack.security.enabled=false to preserve plain-HTTP behavior
  the HA component expects; override via ES_SETTING_XPACK_SECURITY_ENABLED
- Fix the env_vars option, which never worked (the image has no
  s6-overlay, so the cont-init stack never ran)
- Remove the ingest-attachment plugin install (bundled since ES 8.0,
  which broke the 8.x build)
- Replace line-number-based entrypoint patching with a proper init
  script sourced via a pattern-anchored injection
- Add updater.json pinned to the 8.19 line to prevent version drift and
  accidental 9.x jumps

Fixes #2849

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 11:07:24 +02:00
github-actions
34135ba471 GitHub bot: changelog [nobuild] 2026-07-14 08:07:54 +00:00
25 changed files with 501 additions and 370 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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`.

View File

@@ -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).
---

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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}"

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
exec claude "$@"

View File

@@ -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 -- "$@"

View File

@@ -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"

View File

@@ -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`

View File

@@ -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).

View File

@@ -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

View File

@@ -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

View File

@@ -1 +0,0 @@
#!/bin/bash

View 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

View 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"
}