Compare commits

...

64 Commits

Author SHA1 Message Date
Alexandre
26c26e3668 Update ownership to user 'abc' in 20-folders.sh
Change ownership of specified directories to user 'abc'.
2026-07-15 17:29:54 +02:00
github-actions
64c7f2dd5b GitHub bot: changelog [nobuild] 2026-07-15 15:24:28 +00:00
Alexandre
259517289a build 2026-07-15 17:12:15 +02:00
github-actions
b26da2e161 GitHub bot: changelog [nobuild] 2026-07-15 15:08:23 +00:00
Alexandre
9ec6c44f03 Update config.yaml 2026-07-15 17:06:02 +02:00
Alexandre
ff43c4eeba Update CHANGELOG.md 2026-07-15 17:05:51 +02:00
Alexandre
a1e68ee807 Merge pull request #2867 from crazyrokr/feature/gitea-ssl-healthcheck
Fix Gitea HEALTHCHECK in case of SSL setup
2026-07-15 17:05:04 +02:00
Maksim Kashapov
2a1412d957 Fix Gitea HEALTHCHECK in case of SSL setup 2026-07-15 16:10:54 +02:00
github-actions
b328ae242f Github bot : issues linked to readme 2026-07-15 14:01:27 +00:00
Alexandre
9c0521da49 Merge pull request #2865 from alexbelgium/fix/claude-wrapper-headroom-path
fix(claude_desktop): headroom wrapper path + TokenSave startup corruption hardening
2026-07-15 14:58:12 +02:00
alexbelgium
7d2c6eb9b2 fix(claude_desktop): self-heal TokenSave graph against corruption on startup
The startup indexer chose init vs sync purely on whether
.tokensave/tokensave.db existed, so an interrupted init or a hard
add-on stop mid-write could leave a partial or malformed SQLite graph
that every subsequent boot then ran `sync` against, failing (and
staying broken) forever.

Prepare each configured repo defensively instead:
- serialize the operation under a startup-scoped flock so an
  overlapping restart or a mid-boot git post-commit/checkout sync hook
  can't write the same DB concurrently;
- refresh an existing index with a retried incremental sync, since
  SQLITE_BUSY from lock contention is transient, not corruption;
- quarantine a genuinely unreadable index (sync still failing after
  retries) or a half-written one (an interrupted init, detected via a
  sentinel file) to .tokensave/corrupt-<timestamp>/ and rebuild from
  scratch, so the graph self-heals rather than propagating corruption.

All file operations run as the abc runtime user because the repo
.tokensave directory is outside this script's final ownership pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 14:42:41 +02:00
alexbelgium
f990177df8 fix(claude_desktop): resolve headroom binary via PATH in claude wrapper
The /usr/local/bin/claude wrapper hardcoded HEADROOM_BIN as
/usr/local/bin/headroom, but the binary is installed at
/usr/bin/headroom (symlink to /lsiopy/bin/headroom). The -x check
therefore always failed and terminal Claude Code sessions never
routed through the Headroom proxy at 127.0.0.1:8787.

Resolve the binary with "command -v headroom" instead; an empty
result still fails the -x check safely and falls back to launching
Claude Code directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 14:32:35 +02:00
Alexandre
005275315a Merge pull request #2863 from alexbelgium/feat/claude_desktop-ha-api-helper nobuild
feat(claude_desktop): add ha-cli Core-API helper for configuring Home Assistant
2026-07-15 14:25:21 +02:00
alexbelgium
0223cc3511 fix(claude_desktop): address ha-cli review findings
- Check ha_mcp_token before SUPERVISOR_TOKEN: this add-on always sets
  homeassistant_api, so the admin-equivalent Supervisor token was always
  present and silently shadowed a user's deliberately scoped-down
  ha_mcp_token, defeating the documented scoping path (Codex P1).
- Make ha-cli itself refuse to run when enable_ha_api_helper is false,
  instead of only removing the CLAUDE.md guidance text — disabling the
  option now actually disables the helper (Codex P2).
- Normalize HA_BASE_URL to include /api when the user omits it, so REST
  calls don't 404 (CodeRabbit).
- Read/write CLAUDE.md with explicit UTF-8 in the ha-api-helper removal
  block, matching the emoji/special characters Claude tends to write
  there (CodeRabbit).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 14:24:17 +02:00
alexbelgium
e5983f4718 feat(claude_desktop): add ha-cli Core-API helper for configuring Home Assistant
Ship a `ha-cli` command that lets Claude configure Home Assistant
(automations, scripts, scenes, helpers, dashboards, registries, service
calls) through the Home Assistant Core API instead of a /config filesystem
mount, so secrets.yaml and other add-ons' credentials stay out of reach.

It authenticates automatically with the add-on's SUPERVISOR_TOKEN via the
Supervisor Core-API proxy (no token setup), with optional HA_TOKEN /
ha_mcp_token overrides for a scoped Home Assistant user. A managed guidance
block in ~/.claude/CLAUDE.md tells Claude Code to use the helper and to
confirm before writes. Gated by the new enable_ha_api_helper option
(default on). Adds the websockets dependency for the WebSocket subcommand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 14:05:29 +02:00
Alexandre
587121cb1b Update config.yaml 2026-07-15 13:29:35 +02:00
Alexandre
29f2cfd198 Update 81-claude_update.sh 2026-07-15 13:29:07 +02:00
Alexandre
3329c4b40d nobuild 2026-07-15 13:17:03 +02:00
github-actions
9471dffcca GitHub bot: sanitize (spaces + LF endings) & chmod [nobuild] 2026-07-15 09:18:15 +00:00
Alexandre
215327e439 Merge pull request #2862 from alexbelgium/agent/claude-bypass-nonroot
Fix Claude bypass permissions under root PUID
2026-07-15 11:17:27 +02:00
Alexandre
8ecbfccdcd Document Claude bypass root fix 2026-07-15 11:05:49 +02:00
Alexandre
5a2efc4f9e Document non-root Claude bypass runtime 2026-07-15 11:04:27 +02:00
Alexandre
ef0aecbde6 Bump Claude Desktop add-on version 2026-07-15 11:02:55 +02:00
Alexandre
7a6ad72617 Diagnose Claude bypass runtime identity 2026-07-15 11:02:23 +02:00
Alexandre
b5985f230e Restore effective Claude runtime ownership 2026-07-15 11:01:22 +02:00
Alexandre
d48d1f4d8d Drop root for Claude bypass launches 2026-07-15 11:01:06 +02:00
Alexandre
6bf379bffc Use effective Claude runtime ownership 2026-07-15 10:59:49 +02:00
Alexandre
5d577ad954 Run Claude bypass mode as non-root 2026-07-15 10:59:09 +02:00
GitHub Actions
85dab2ae6e Revert "Update config.yaml"
This reverts commit ad50abc365.
2026-07-15 08:27:28 +00:00
Alexandre
ad50abc365 Update config.yaml 2026-07-15 10:25:14 +02:00
github-actions
dc280a5caf GitHub bot: sanitize (spaces + LF endings) & chmod [nobuild] 2026-07-15 08:19:42 +00:00
Alexandre
c0c1df8c27 Merge pull request #2861 from alexbelgium/agent/claude-tools-hardening
Improve Claude Desktop optimization tooling
2026-07-15 10:18:44 +02:00
Alexandre
a38bc75f95 Report Claude permission mode in diagnostics 2026-07-15 10:11:00 +02:00
Alexandre
8073317150 Document Claude permission modes 2026-07-15 10:09:44 +02:00
Alexandre
15505a265f Trust configured TokenSave repositories 2026-07-15 10:08:34 +02:00
Alexandre
d1ceafebe8 Fix actionlint amd64 asset mapping 2026-07-15 10:08:08 +02:00
Alexandre
c76b257f5d Persist Claude permission mode 2026-07-15 10:05:29 +02:00
Alexandre
321f2fde73 Apply Claude permission mode in wrapper 2026-07-15 10:04:48 +02:00
Alexandre
45f88307d4 Add Claude permission modes 2026-07-15 10:04:23 +02:00
Alexandre
7cd82b2758 Fix validator release asset lookup 2026-07-15 09:20:43 +02:00
Alexandre
66d3886b80 Improve Claude optimization tooling 2026-07-15 09:17:05 +02:00
Alexandre
bdd56b8057 Improve Claude optimization tooling 2026-07-15 09:16:08 +02:00
Alexandre
9c55193c38 Improve Claude optimization tooling 2026-07-15 09:15:18 +02:00
Alexandre
6b6233e1fa Improve Claude optimization tooling 2026-07-15 09:14:23 +02:00
Alexandre
22951ac4f5 Improve Claude optimization tooling 2026-07-15 09:12:44 +02:00
Alexandre
006f3052ac Improve Claude optimization tooling 2026-07-15 09:12:14 +02:00
Alexandre
0ff2e8f783 Improve Claude optimization tooling 2026-07-15 09:11:58 +02:00
Alexandre
3c8a32199e Improve Claude optimization tooling 2026-07-15 09:11:46 +02:00
Alexandre
c8d706c9de Improve Claude optimization tooling 2026-07-15 09:11:30 +02:00
Alexandre
c04f3e288c Improve Claude optimization tooling 2026-07-15 09:11:20 +02:00
github-actions
f6c2bef0be GitHub bot: changelog [nobuild] 2026-07-14 17:59:29 +00:00
Alexandre
f8c447ed19 Update config.yaml 2026-07-14 19:46:49 +02:00
Alexandre
bd82e72ffa Remove gnome-keyring from Dockerfile
Removed gnome-keyring from the list of packages to install.
2026-07-14 19:46:37 +02:00
github-actions
a3ecb38fea GitHub bot : README updated 2026-07-14 17:26:53 +00:00
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
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
42 changed files with 1165 additions and 774 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

@@ -258,7 +258,7 @@ If you want to do add the repository manually, please follow the procedure highl
![amd64][amd64-badge]
![ingress][ingress-badge]
&#10003; ![image](https://api.iconify.design/mdi/robot-happy.svg) [Claude Desktop](claude_desktop/) : Claude Desktop and a persistent Claude Code web terminal
&#10003; ![image](https://api.iconify.design/mdi/robot-happy.svg) [Claude Desktop](claude_desktop/) : Claude Desktop with Headroom MCP context compression and RTK acceleration
&emsp;&emsp;![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fclaude_desktop%2Fconfig.yaml)
![Update](https://img.shields.io/badge/dynamic/json?label=Updated&query=%24.last_update&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fclaude_desktop%2Fupdater.json)
@@ -313,6 +313,7 @@ If you want to do add the repository manually, please follow the procedure highl
&#10003; [Elasticsearch server](elasticsearch/) : Free and Open, Distributed, RESTful Search Engine
&emsp;&emsp;![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Felasticsearch%2Fconfig.yaml)
![Update](https://img.shields.io/badge/dynamic/json?label=Updated&query=%24.last_update&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Felasticsearch%2Fupdater.json)
![aarch64][aarch64-badge]
![amd64][amd64-badge]

View File

@@ -1,10 +0,0 @@
## 0.144.3-2 (14-07-2026)
- Initial ChatGPT Codex add-on.
- Added a persistent, administrator-only Home Assistant ingress terminal backed by tmux.
- Made `headroom wrap codex` the default launch path.
- Configured Docker builds to install the latest stable Codex, Headroom, RTK, ttyd, and Rust toolchain versions without hard-coded tool version pins.
- Added RTK native Codex initialization and savings reporting.
- Added device-code authentication and direct Codex fallback helpers.
- Added persistent configuration, GitHub CLI integration, mount support, and safe defaults.
- Made the default workspace follow a custom `data_location`.

View File

@@ -1,157 +0,0 @@
#============================#
# ALEXBELGIUM'S DOCKERFILE #
#============================#
#=== Home Assistant Addon ===#
#################
# 1 Build Image #
#################
ARG BUILD_FROM
ARG BUILD_VERSION
FROM rust:bookworm AS rtk-builder
RUN set -eux; \
rtk_version="$(git ls-remote --tags --refs --sort=-v:refname \
https://github.com/rtk-ai/rtk.git 'refs/tags/v*' \
| awk -F/ '$3 ~ /^v[0-9]+\.[0-9]+\.[0-9]+$/ { print $3; exit }')"; \
test -n "$rtk_version"; \
git clone --depth 1 --branch "$rtk_version" https://github.com/rtk-ai/rtk.git /src/rtk; \
cd /src/rtk; \
cargo build --release --locked; \
install -D -m 0755 target/release/rtk /out/rtk; \
/out/rtk --version
FROM ${BUILD_FROM}
##################
# 2 Modify Image #
##################
ENV S6_CMD_WAIT_FOR_SERVICES=1 \
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
S6_SERVICES_GRACETIME=0 \
HEADROOM_CONTEXT_TOOL=rtk
USER root
VOLUME [ "/sys/fs/cgroup" ]
ARG TEMPLATE_BASE_URL="https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates"
##################
# 3 Install apps #
##################
COPY rootfs/ /
RUN find /etc/cont-init.d /etc/s6-overlay /usr/local/bin \
-type f \( -name "*.sh" -o -name "run" -o -name "finish" -o -path "/usr/local/bin/*" \) \
-print -exec chmod +x {} \;
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
curl \
git \
gh \
jq \
less \
nano \
openssh-client \
python3-pip \
ripgrep \
tmux && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install the latest stable official Codex static binary for the target architecture.
RUN set -eux; \
case "$(dpkg --print-architecture)" in \
amd64) codex_arch="x86_64" ;; \
arm64) codex_arch="aarch64" ;; \
*) echo "Unsupported architecture: $(dpkg --print-architecture)" >&2; exit 1 ;; \
esac; \
archive="/tmp/codex.tar.gz"; \
curl -fsSL --retry 3 --retry-delay 2 \
-o "$archive" \
"https://github.com/openai/codex/releases/latest/download/codex-${codex_arch}-unknown-linux-musl.tar.gz"; \
tar -xzf "$archive" -C /tmp; \
install -m 0755 "/tmp/codex-${codex_arch}-unknown-linux-musl" /usr/local/bin/codex; \
rm -f "$archive" "/tmp/codex-${codex_arch}-unknown-linux-musl"; \
codex --version
# Install the latest stable ttyd binary for the Home Assistant ingress terminal.
RUN set -eux; \
case "$(dpkg --print-architecture)" in \
amd64) ttyd_arch="x86_64" ;; \
arm64) ttyd_arch="aarch64" ;; \
*) echo "Unsupported architecture: $(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/latest/download/ttyd.${ttyd_arch}"; \
chmod 0755 /usr/local/bin/ttyd; \
ttyd --version
COPY --from=rtk-builder /out/rtk /usr/local/bin/rtk
RUN rtk --version && \
pip3 install --upgrade --break-system-packages --no-cache-dir "headroom-ai[proxy,code,mcp]" && \
headroom --version
ARG MODULES="00-banner.sh 00-global_var.sh 01-custom_script.sh 00-local_mounts.sh 00-smb_mounts.sh 90-dns_set.sh"
RUN curl -fsSL --retry 3 --retry-delay 2 \
-o /ha_automodules.sh "${TEMPLATE_BASE_URL}/ha_automodules.sh" && \
chmod 744 /ha_automodules.sh && \
/ha_automodules.sh "$MODULES" && \
rm /ha_automodules.sh
################
# 4 Entrypoint #
################
RUN curl -fsSL --retry 3 --retry-delay 2 \
-o /ha_entrypoint.sh "${TEMPLATE_BASE_URL}/ha_entrypoint.sh" && \
curl -fsSL --retry 3 --retry-delay 2 \
-o /usr/local/lib/bashio-standalone.sh "${TEMPLATE_BASE_URL}/bashio-standalone.sh" && \
chmod 0777 /ha_entrypoint.sh && \
chmod 0755 /usr/local/lib/bashio-standalone.sh
ENTRYPOINT [ "/usr/bin/env" ]
CMD [ "/ha_entrypoint.sh" ]
############
# 5 Labels #
############
ARG BUILD_ARCH
ARG BUILD_DATE
ARG BUILD_DESCRIPTION
ARG BUILD_NAME
ARG BUILD_REF
ARG BUILD_REPOSITORY
ARG BUILD_VERSION
ENV BUILD_VERSION="${BUILD_VERSION}"
LABEL \
io.hass.name="${BUILD_NAME}" \
io.hass.description="${BUILD_DESCRIPTION}" \
io.hass.arch="${BUILD_ARCH}" \
io.hass.type="addon" \
io.hass.version=${BUILD_VERSION} \
maintainer="alexbelgium (https://github.com/alexbelgium)" \
org.opencontainers.image.title="${BUILD_NAME}" \
org.opencontainers.image.description="${BUILD_DESCRIPTION}" \
org.opencontainers.image.vendor="Home Assistant Add-ons" \
org.opencontainers.image.authors="alexbelgium (https://github.com/alexbelgium)" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.url="https://github.com/alexbelgium" \
org.opencontainers.image.source="https://github.com/${BUILD_REPOSITORY}" \
org.opencontainers.image.documentation="https://github.com/${BUILD_REPOSITORY}/blob/master/chatgpt_codex/README.md" \
org.opencontainers.image.created=${BUILD_DATE} \
org.opencontainers.image.revision=${BUILD_REF} \
org.opencontainers.image.version=${BUILD_VERSION}
#################
# 6 Healthcheck #
#################
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s \
CMD curl -fsS http://127.0.0.1:7681/ > /dev/null || exit 1

View File

@@ -1,112 +0,0 @@
# Home Assistant add-on: ChatGPT Codex
![Supports aarch64 Architecture][aarch64-shield]
![Supports amd64 Architecture][amd64-shield]
![Project Maintenance][maintenance-shield]
Run the official OpenAI Codex CLI in a persistent Home Assistant ingress terminal. The optimized path uses `headroom wrap codex`, with RTK handling command-output compression before results reach Codex.
> The repository already contains an unrelated add-on named **Codex** for comic archives. This coding-agent add-on therefore uses the slug `chatgpt_codex`.
## Features
- Latest stable Codex, Headroom, RTK, ttyd, and Rust toolchain versions are resolved during every Docker build; tool versions are not pinned in the Dockerfile.
- Official Codex CLI static binary for `amd64` and `aarch64`.
- Home Assistant authenticated, administrator-only ingress; no unauthenticated terminal port is exposed.
- Persistent `$HOME`, Codex authentication, settings, sessions, Headroom state, and RTK statistics.
- Persistent `tmux` session that survives browser disconnects.
- `headroom wrap codex` as the default launch path.
- Baked-in RTK with native Codex initialization.
- Optional Headroom output shaping and code-aware compression.
- Direct Codex fallback for troubleshooting.
- Device-code login helper designed for a remote or headless container.
- Baked-in Git, GitHub CLI, ripgrep, jq, SSH client, and common terminal tools.
- Optional GitHub CLI authentication and Git author configuration.
- Optional extra apt and pip packages.
- Local and SMB mount support through the repository standard modules.
## Installation and first login
1. Install **ChatGPT Codex** from this add-on repository.
2. Keep the default `data_location` and `workspace`, or select writable mounted paths.
3. Start the add-on and open its web UI.
4. Codex starts automatically through Headroom.
5. When prompted to authenticate, follow the device-code instructions. You can also exit Codex and run:
```shell
codex-login
```
Codex supports ChatGPT sign-in and API-key authentication. The device-code flow is the recommended option for this headless add-on.
## Launch commands
Optimized default:
```shell
codex-headroom
```
This runs:
```shell
headroom wrap codex
```
Headroom starts its local proxy, configures Codex routing and MCP support, and uses RTK as the CLI context tool.
Direct troubleshooting path:
```shell
codex-direct
```
Check optimization status and measured savings:
```shell
headroom doctor
headroom perf
rtk gain
```
## Persistence
The terminal attaches every browser connection to the same `tmux` session. Closing the browser detaches the client but does not stop Codex or commands running in the session.
Persistent data is stored below `data_location`:
- Codex state: `~/.codex`
- Headroom state and metrics: `~/.headroom`
- RTK state: its normal paths below the persistent home
- Default workspace: `~/workspace`
## Options
| Option | Default | Description |
| --- | --- | --- |
| `data_location` | `/data/data` | Persistent home. Must be below `/data`, `/share`, `/media`, `/config`, or `/mnt`. |
| `workspace` | `<data_location>/workspace` | Initial project directory. Leave empty to follow `data_location`. |
| `PUID` / `PGID` | `0` / `0` | Runtime user and group used by the LinuxServer `abc` account. |
| `TZ` | | Optional timezone, for example `Europe/Brussels`. |
| `auto_start_codex` | `true` | Start Codex automatically when the tmux session is first created. |
| `use_headroom` | `true` | Use `headroom wrap codex`; disabling this starts Codex directly. |
| `headroom_output_shaper` | `true` | Enable Headroom output-token shaping. |
| `headroom_code_aware` | `true` | Enable Headroom AST-aware code compression. |
| `github_token` | | Authenticate GitHub CLI and Git operations. |
| `github_username` / `github_email` | | Configure the global Git author. |
| `additional_apps` | | Comma-separated Debian packages installed at startup. |
| `additional_pip` | | Comma-separated Python packages installed at startup. |
| `localdisks` / `networkdisks` | | Optional local-disk and SMB mounts supported by the repository modules. |
| `env_vars` | `[]` | Additional environment variables exported in the container. |
Configuration changes affecting the launch command apply to a newly created tmux session. To recreate it, exit Codex and run `tmux kill-session -t codex`, then reopen the add-on web UI.
## Security
The add-on deliberately does not enable Codex approval or sandbox bypass flags. Codex can execute commands and edit files available inside the configured workspace, so only mount locations you intend it to access.
The terminal is exposed only through Home Assistant administrator-only ingress. Do not add an unauthenticated direct port mapping. Treat `github_token`, Codex authentication data, and the persistent home as secrets and include them only in trusted backups.
[aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg
[amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg
[maintenance-shield]: https://img.shields.io/maintenance/yes/2026.svg

View File

@@ -1,6 +0,0 @@
{
"build_from": {
"aarch64": "ghcr.io/linuxserver/baseimage-debian:arm64v8-bookworm",
"amd64": "ghcr.io/linuxserver/baseimage-debian:amd64-bookworm"
}
}

View File

@@ -1,68 +0,0 @@
arch:
- aarch64
- amd64
description: "Persistent OpenAI Codex web terminal optimized with Headroom and RTK"
devices:
- /dev/fuse
environment:
HOME: /data/data
PGID: "0"
PUID: "0"
TERM: xterm-256color
image: ghcr.io/alexbelgium/chatgpt_codex-{arch}
ingress: true
ingress_port: 7681
ingress_stream: true
init: false
map:
- addon_config:rw
- share:rw
- media:rw
- ssl
name: ChatGPT Codex
options:
env_vars: []
DNS_server: 8.8.8.8
data_location: /data/data
workspace: ""
PUID: 0
PGID: 0
auto_start_codex: true
use_headroom: true
headroom_output_shaper: true
headroom_code_aware: true
github_token: ""
github_username: ""
github_email: ""
additional_apps: ""
additional_pip: ""
panel_icon: mdi:code-braces-box
privileged:
- SYS_ADMIN
- DAC_READ_SEARCH
schema:
env_vars:
- name: match(^[A-Za-z0-9_]+$)
value: str?
DNS_server: str?
data_location: str?
workspace: str?
PUID: int
PGID: int
TZ: match([A-Z][a-z]*./[A-Z][a-z]*.)?
auto_start_codex: bool
use_headroom: bool
headroom_output_shaper: bool
headroom_code_aware: bool
github_token: password?
github_username: str?
github_email: str?
additional_apps: str?
additional_pip: str?
localdisks: str?
networkdisks: str?
slug: chatgpt_codex
tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "0.144.3-3"

View File

@@ -1,46 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
set -o pipefail
PUID="$(bashio::config 'PUID')"
PGID="$(bashio::config 'PGID')"
LOCATION="$(bashio::config 'data_location')"
if [ -z "$LOCATION" ] || [ "$LOCATION" = "null" ]; then
LOCATION="/data/data"
fi
case "$LOCATION" in
/data/* | /share/* | /media/* | /config/* | /mnt/*)
;;
*)
bashio::log.fatal "data_location must be below /data, /share, /media, /config, or /mnt"
exit 1
;;
esac
if [ -L "$LOCATION" ]; then
bashio::log.fatal "data_location must not be a symbolic link"
exit 1
fi
bashio::log.info "Using persistent home: $LOCATION"
install -d -m 0750 -o "$PUID" -g "$PGID" "$LOCATION"
install -d -m 0750 -o "$PUID" -g "$PGID" "$LOCATION/.codex" "$LOCATION/.headroom"
install -d -m 0755 /tmp/cache /run/s6/container_environment
sed -i "s|^\(abc:[^:]*:[^:]*:[^:]*:[^:]*:\)[^:]*|\1$LOCATION|" /etc/passwd
for variable in HOME CODEX_HOME HEADROOM_WORKSPACE_DIR XDG_CACHE_HOME; do
case "$variable" in
HOME) value="$LOCATION" ;;
CODEX_HOME) value="$LOCATION/.codex" ;;
HEADROOM_WORKSPACE_DIR) value="$LOCATION/.headroom" ;;
XDG_CACHE_HOME) value="/tmp/cache" ;;
esac
printf '%s' "$value" > "/run/s6/container_environment/$variable"
done
chown -R "$PUID:$PGID" "$LOCATION/.codex" "$LOCATION/.headroom"
chown "$PUID:$PGID" "$LOCATION"

View File

@@ -1,33 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
set -o pipefail
if bashio::config.has_value 'additional_apps'; then
packages="$(bashio::config 'additional_apps')"
apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10
for package in ${packages//,/ }; do
bashio::log.info "Installing apt package: $package"
apt-get install -y --no-install-recommends "$package"
done
apt-get clean
rm -rf /var/lib/apt/lists/*
fi
if bashio::config.has_value 'additional_pip'; then
packages="$(bashio::config 'additional_pip')"
for package in ${packages//,/ }; do
bashio::log.info "Installing pip package: $package"
pip3 install --break-system-packages "$package"
done
fi
if bashio::config.has_value 'TZ'; then
timezone="$(bashio::config 'TZ')"
if [ ! -e "/usr/share/zoneinfo/$timezone" ]; then
bashio::log.fatal "Invalid timezone: $timezone"
exit 1
fi
ln -snf "/usr/share/zoneinfo/$timezone" /etc/localtime
printf '%s\n' "$timezone" > /etc/timezone
fi

View File

@@ -1,55 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
set -o pipefail
PUID="$(bashio::config 'PUID')"
PGID="$(bashio::config 'PGID')"
if ! command -v codex > /dev/null 2>&1; then
bashio::log.fatal "Codex CLI is not available"
exit 1
fi
if ! command -v headroom > /dev/null 2>&1; then
bashio::log.fatal "Headroom is not available"
exit 1
fi
if ! command -v rtk > /dev/null 2>&1; then
bashio::log.fatal "RTK is not available"
exit 1
fi
bashio::log.info "Codex: $(codex --version 2>&1 | head -n 1)"
bashio::log.info "Headroom: $(headroom --version 2>&1 | head -n 1)"
bashio::log.info "RTK: $(rtk --version 2>&1 | head -n 1)"
# Configure RTK's native Codex integration ahead of the first wrapped session.
if ! s6-setuidgid abc env \
HOME="$HOME" \
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" \
PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin" \
RTK_NONINTERACTIVE=1 \
rtk init -g --codex; then
bashio::log.warning "RTK Codex initialization failed; Headroom will retry when wrapping Codex"
fi
for key in CODEX_AUTO_START CODEX_USE_HEADROOM HEADROOM_OUTPUT_SHAPER HEADROOM_CODE_AWARE_ENABLED; do
case "$key" in
CODEX_AUTO_START)
bashio::config.true 'auto_start_codex' && value="1" || value="0"
;;
CODEX_USE_HEADROOM)
bashio::config.true 'use_headroom' && value="1" || value="0"
;;
HEADROOM_OUTPUT_SHAPER)
bashio::config.true 'headroom_output_shaper' && value="1" || value="0"
;;
HEADROOM_CODE_AWARE_ENABLED)
bashio::config.true 'headroom_code_aware' && value="1" || value="0"
;;
esac
printf '%s' "$value" > "/run/s6/container_environment/$key"
done
printf '%s' 'rtk' > /run/s6/container_environment/HEADROOM_CONTEXT_TOOL
chown -R "$PUID:$PGID" "$HOME/.codex" "$HOME/.headroom"

View File

@@ -1,29 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
set -o pipefail
if bashio::config.has_value 'github_username'; then
s6-setuidgid abc git config --global user.name "$(bashio::config 'github_username')"
fi
if bashio::config.has_value 'github_email'; then
s6-setuidgid abc git config --global user.email "$(bashio::config 'github_email')"
fi
if bashio::config.has_value 'github_token'; then
token="$(bashio::config 'github_token')"
if s6-setuidgid abc env -u GH_TOKEN -u GITHUB_TOKEN gh auth status --hostname github.com > /dev/null 2>&1; then
bashio::log.info "GitHub CLI is already authenticated"
else
bashio::log.info "Authenticating GitHub CLI"
printf '%s\n' "$token" | s6-setuidgid abc env -u GH_TOKEN -u GITHUB_TOKEN \
gh auth login --hostname github.com --with-token || \
bashio::log.warning "GitHub CLI authentication failed"
fi
s6-setuidgid abc env -u GH_TOKEN -u GITHUB_TOKEN \
gh auth setup-git --hostname github.com || \
bashio::log.warning "GitHub CLI git credential setup failed"
else
bashio::log.info "Set github_token to authenticate gh and Git operations"
fi

View File

@@ -1,54 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
set -o pipefail
port=7681
workspace="$(bashio::config 'workspace')"
if [ -z "$workspace" ] || [ "$workspace" = "null" ]; then
workspace="$HOME/workspace"
fi
if [[ "$workspace" != /* ]]; then
bashio::log.fatal "workspace must be an absolute path"
exec sleep infinity
fi
if [ -L "$workspace" ]; then
bashio::log.fatal "workspace must not be a symbolic link"
exec sleep infinity
fi
workspace="$(realpath -m -- "$workspace")"
case "$workspace" in
"$HOME" | "$HOME"/* | /share/* | /media/* | /mnt/* | /data/* | /config/*)
;;
*)
bashio::log.fatal "workspace must be below the persistent home, /share, /media, /mnt, /data, or /config"
exec sleep infinity
;;
esac
if [ ! -e "$workspace" ]; then
install -d -m 0750 -o abc -g abc "$workspace"
elif [ ! -d "$workspace" ]; then
bashio::log.fatal "workspace is not a directory: $workspace"
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.fatal "workspace must be readable, writable, and searchable by user abc: $workspace"
exec sleep infinity
fi
export CODEX_TERMINAL_WORKSPACE="$workspace"
bashio::log.info "Starting persistent Codex terminal on Home Assistant ingress port $port"
exec s6-setuidgid abc ttyd \
-p "$port" \
-W \
-O \
-t disableLeaveAlert=true \
-t fontSize=14 \
/usr/local/bin/codex-terminal-shell

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
exec codex "$@"

View File

@@ -1,15 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
export HEADROOM_CONTEXT_TOOL="rtk"
if ! command -v headroom > /dev/null 2>&1; then
echo "Headroom is unavailable; launching Codex directly." >&2
exec codex "$@"
fi
if [ "$#" -eq 0 ]; then
exec headroom wrap codex
fi
exec headroom wrap codex -- "$@"

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
exec codex login --device-auth "$@"

View File

@@ -1,33 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
export SHELL="/bin/bash"
export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
workspace="${CODEX_TERMINAL_WORKSPACE:-${HOME}/workspace}"
session_name="${CODEX_TMUX_SESSION:-codex}"
if [ ! -d "$workspace" ]; then
echo "Codex workspace does not exist: $workspace" >&2
exit 1
fi
new_session=0
if ! tmux has-session -t "$session_name" 2> /dev/null; then
tmux new-session -d -s "$session_name" -c "$workspace" /bin/bash -l
new_session=1
fi
if [ "$new_session" -eq 1 ]; then
tmux send-keys -t "$session_name" \
"printf '\\nChatGPT Codex add-on\\n login: codex-login\\n optimized: codex-headroom\\n direct: codex-direct\\n savings: rtk gain && headroom perf\\n\\n'" C-m
if [ "${CODEX_AUTO_START:-1}" = "1" ]; then
if [ "${CODEX_USE_HEADROOM:-1}" = "1" ]; then
tmux send-keys -t "$session_name" "codex-headroom" C-m
else
tmux send-keys -t "$session_name" "codex-direct" C-m
fi
fi
fi
exec tmux attach-session -t "$session_name"

View File

@@ -1,12 +0,0 @@
{
"github_beta": false,
"github_fulltag": false,
"github_havingasset": true,
"github_tagfilter": "rust-v",
"last_update": "2026-07-14",
"repository": "alexbelgium/hassio-addons",
"slug": "chatgpt_codex",
"source": "github",
"upstream_repo": "openai/codex",
"upstream_version": "0.144.3"
}

View File

@@ -1,11 +1,46 @@
## 1.17 (14-07-2026)
## 1.25 (15-07-2026)
- Minor bugs fixed
## 1.24 (15-07-2026)
- Fix the `/usr/local/bin/claude` wrapper never routing terminal Claude Code sessions through the Headroom proxy: it hardcoded `HEADROOM_BIN="/usr/local/bin/headroom"` while the binary is installed at `/usr/bin/headroom`, so the executable check always failed and the wrapper fell back to launching Claude Code directly. Resolve the binary with `command -v headroom` instead.
- Harden startup TokenSave indexing so an interrupted `init`/`sync` or a hard add-on stop can no longer leave a corrupt semantic graph that fails every subsequent boot. Each configured repository is now prepared under a startup-scoped `flock` (serialised against overlapping restarts and mid-boot git sync hooks); an existing index is refreshed with a retried incremental `sync` (transient `SQLITE_BUSY` no longer looks like corruption); and only a genuinely unreadable index — or a half-written one flagged by an `init` sentinel — is quarantined to `.tokensave/corrupt-<timestamp>/` and rebuilt from scratch, so the graph self-heals instead of propagating corruption.
## 1.23 (15-07-2026)
- Add a `ha-cli` helper that lets Claude configure Home Assistant (automations, scripts, scenes, helpers, dashboards, area/label/floor/entity registries, and service calls) through the Home Assistant Core API instead of a filesystem mount. It authenticates automatically with the add-on's `SUPERVISOR_TOKEN` via the Supervisor Core-API proxy (no token setup), and deliberately cannot reach `configuration.yaml`/`secrets.yaml` or other add-ons' credentials. Toggle with the new `enable_ha_api_helper` option (default on), which also controls a managed guidance block appended to `~/.claude/CLAUDE.md`.
## 1.21 (15-07-2026)
- Fix Claude Code bypass permissions being rejected when the add-on uses its default root `PUID`.
- In `permission_mode: bypass`, remap the shared `abc` Desktop runtime to an unused non-root UID before storage ownership and Selkies startup, while retaining its configured primary group for mounted-path access.
- Make folder setup and final Claude configuration ownership follow the effective `abc` identity instead of the configured root UID.
- Drop root console invocations of the add-on's `/usr/local/bin/claude` wrapper to the non-root `abc` runtime before passing `--dangerously-skip-permissions`.
- Extend `claude-tools-doctor.sh` with configured/effective UID and GID checks for bypass mode.
## 1.20 (15-07-2026)
- Complete the TokenSave Claude Code integration at startup: install its MCP server, permissions, PreToolUse/UserPromptSubmit/Stop hooks, global guidance, and Git synchronization hooks instead of registering only `tokensave serve`.
- Add `tokensave_project_paths` for explicit per-repository initialization and incremental synchronization; no repositories are scanned or indexed unless listed.
- Route PATH-based Claude Code launches through the already-supervised Headroom proxy by default with a recursion-safe `/usr/local/bin/claude` wrapper; fall back to the official binary when the proxy is unavailable.
- Pass the local proxy URL explicitly to the Headroom MCP server, while retaining MCP-only integration for the Desktop Electron application.
- Keep the unauthenticated Headroom dashboard container-local by default; add `expose_headroom_dashboard` and leave port `8787/tcp` unmapped until explicitly enabled.
- Fix the hourly gains report so Headroom no longer suppresses RTK output, add TokenSave gains, and gate each tool on its actual add-on option.
- Add `claude-tools-doctor.sh` to inspect binaries, redacted MCP registrations, hooks, proxy health, routing, project indexes, and gains.
- Install local validation tools (`jq`, `shellcheck`, `yamllint`, current `hadolint`, and current `actionlint`) to reduce avoidable CI round-trips.
- Disable the unpinned third-party Caveman startup installer by default; it remains opt-in.
## 1.19 (14-07-2026)
- Minor bugs fixed
## 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`.
- 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.
@@ -13,7 +48,7 @@
- Minor bugs fixed
## 1.15 (13-07-2026)
- Minor bugs fixed
## ubunturesolute-version-6dc44b0e (2026-07-13)
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
## 1.14 (10-07-2026)

View File

@@ -34,6 +34,7 @@ RUN cargo install tokensave --version "${TOKENSAVE_VERSION}" --locked --root /ou
/out/bin/tokensave --version
FROM ${BUILD_FROM}
ARG BUILD_ARCH
##################
# 2 Modify Image #
@@ -69,17 +70,21 @@ RUN curl -fsSL --retry 3 --retry-delay 2 \
# 3 Install apps #
##################
# Add rootfs
# Add rootfs. Only the directories this add-on ships scripts in are traversed, so the chmod
# cannot alter executables elsewhere in the image.
COPY rootfs/ /
RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \;
RUN find /etc/cont-init.d /etc/s6-overlay /defaults /usr/local/bin -type f \
\( -name "*.sh" -o -name "run" -o -name "finish" -o -name "ha-cli" \) -print -exec chmod +x {} \; && \
chmod +x /usr/local/bin/claude
# 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, and Python tooling. gnome-keyring provides the
# Secret Service backend Electron safeStorage needs to persist sign-in and dispatch grants.
# Install Claude Desktop, Claude Code, Python tooling, and lightweight local validators.
# 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 && \
@@ -90,15 +95,46 @@ RUN install -d -m 0755 /etc/apt/keyrings && \
claude-desktop \
claude-code \
python3-pip \
gnome-keyring \
libsecret-1-0 \
dbus-x11 \
git \
gh \
ripgrep && \
ripgrep \
jq \
shellcheck \
yamllint && \
test -x /usr/bin/claude && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install the current upstream hadolint and actionlint releases for both supported
# architectures. The GitHub release API resolves the latest asset at build time, so these
# developer tools are intentionally not version-pinned.
RUN set -eux; \
case "${BUILD_ARCH}" in \
amd64) hadolint_arch="x86_64"; actionlint_arch="amd64" ;; \
aarch64) hadolint_arch="arm64"; actionlint_arch="arm64" ;; \
*) echo "Unsupported validation-tools architecture: ${BUILD_ARCH}" >&2; exit 1 ;; \
esac; \
hadolint_name="hadolint-linux-${hadolint_arch}"; \
hadolint_url="$(curl -fsSL https://api.github.com/repos/hadolint/hadolint/releases/latest \
| jq -r --arg name "${hadolint_name}" '.assets[] | select(.name == $name) | .browser_download_url' \
| head -n 1)"; \
test -n "${hadolint_url}"; \
curl -fsSL --retry 3 --retry-delay 2 -o /usr/local/bin/hadolint "${hadolint_url}"; \
chmod 0755 /usr/local/bin/hadolint; \
actionlint_suffix="_linux_${actionlint_arch}.tar.gz"; \
actionlint_url="$(curl -fsSL https://api.github.com/repos/rhysd/actionlint/releases/latest \
| jq -r --arg suffix "${actionlint_suffix}" '.assets[] | select(.name | endswith($suffix)) | .browser_download_url' \
| head -n 1)"; \
test -n "${actionlint_url}"; \
curl -fsSL --retry 3 --retry-delay 2 -o /tmp/actionlint.tar.gz "${actionlint_url}"; \
tar -xzf /tmp/actionlint.tar.gz -C /tmp actionlint; \
install -m 0755 /tmp/actionlint /usr/local/bin/actionlint; \
rm -f /tmp/actionlint /tmp/actionlint.tar.gz; \
hadolint --version; \
actionlint -version
# 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
@@ -106,11 +142,11 @@ 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,
# plus mcp-proxy (stdio->SSE bridge for the Home Assistant MCP server) and uv (fast
# plus mcp-proxy (stdio->HTTP 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]" mcp-proxy uv && \
pip3 install --break-system-packages "headroom-ai[proxy,code,mcp]" mcp-proxy uv websockets && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /root/.cache
@@ -158,7 +194,6 @@ CMD [ "/ha_entrypoint.sh" ]
# 5 Labels #
############
ARG BUILD_ARCH
ARG BUILD_DATE
ARG BUILD_DESCRIPTION
ARG BUILD_NAME

View File

@@ -4,9 +4,9 @@
![Supports amd64 Architecture][amd64-shield]
![Project Maintenance][maintenance-shield]
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.
Run Claude Desktop in a LinuxServer.io Selkies add-on, with Headroom context
compression, RTK Bash-output acceleration, and TokenSave semantic code
intelligence wired in by default.
## Installation
@@ -24,15 +24,39 @@ currently does not include Computer Use or dictation.
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 Code configuration (`~/.claude`), hooks, MCP servers, permissions, and
PATH tools.
- **Claude Desktop** uses Headroom through its MCP tools.
- **Claude Code sessions inside Desktop** get the same MCP servers via
`~/.claude.json` and RTK's `PreToolUse` Bash hook via
`~/.claude/settings.json`.
- **Claude Code sessions inside Desktop** get the same MCP servers, permission
mode, and RTK/TokenSave hooks through the shared Claude Code configuration.
- PATH-based Claude Code launches are routed through the supervised Headroom
proxy when `headroom_wrap_claude_code` is enabled. If a Desktop release calls
`/usr/bin/claude` directly, the session remains functional and still has the
shared permission mode and Headroom MCP tools, but transparent proxy
compression cannot be injected.
- When `permission_mode: bypass` is selected while `PUID` is `0`, the add-on
automatically remaps the shared `abc` desktop account to an unused non-root
UID before Selkies and Claude Desktop start. Claude Code refuses bypass mode
under an effective root UID.
- **gnome-keyring** provides the Secret Service backend Electron needs to
persist sign-in and dispatch permission grants across restarts.
## Optimization layers
The three bundled optimization tools are complementary:
- **RTK** rewrites supported Bash commands so Claude receives compact output.
- **TokenSave** builds a local semantic graph for explicitly selected code
repositories and steers Claude away from repeated Explore/Grep/Read fan-out.
- **Headroom** transparently compresses proxied Claude Code traffic and also
exposes on-demand compress/retrieve/statistics MCP tools to Claude Desktop.
TokenSave's complete Claude integration is installed at startup: MCP server,
permissions, PreToolUse/UserPromptSubmit/Stop hooks, global prompt rules, and
Git synchronization hooks. A repository is indexed only when it is listed in
`tokensave_project_paths`; no automatic filesystem scan is performed.
## Features
- Claude Desktop in single-app Selkies mode with Home Assistant ingress.
@@ -41,65 +65,143 @@ Claude Code configuration (`~/.claude`), hooks, and MCP servers automatically.
- 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.
- Configurable Claude Code permissions: strict prompts, automatic safe-action
approval, or explicit full bypass for trusted installations.
- Automatic non-root runtime enforcement for bypass mode, including root-console
wrapper launches.
- Optional runtime Claude Desktop updates from Anthropic's apt repository.
- Optional extra apt and pip package installation (pip installs use `uv` for
speed).
- Baked-in `git`, GitHub CLI (`gh`), and `ripgrep`.
- Optional extra apt and pip package installation (pip installs use `uv`).
- Baked-in `git`, GitHub CLI (`gh`), `ripgrep`, `jq`, `shellcheck`, `yamllint`,
`hadolint`, and `actionlint`.
- Custom script support through the repository standard `claude_desktop.sh`.
- Bundled optimization tools: Headroom (MCP + local proxy), RTK, tokensave,
and Caveman — each individually switchable.
- Bundled optimization tools: Headroom, RTK, and TokenSave; Caveman remains
available as an opt-in plugin.
- Optional Home Assistant MCP bridge so Claude can query and control Home
Assistant.
- Headroom dashboard exposed on mapped port `8787`.
- Independent hourly savings reports for Headroom, RTK, and TokenSave.
- `claude-tools-doctor.sh` diagnostics for binaries, routing, hooks, MCP
registrations, project indexes, proxy health, permissions, runtime identity,
and gains.
- Low-power defaults for GPU mapping, Selkies frame rate, and volatile caches.
## Options
| Option | Default | Description |
| ------ | ------- | ----------- |
| `PUID` / `PGID` | `0` / `0` | Numeric user and group applied by the LinuxServer initialization. |
| `PUID` / `PGID` | `0` / `0` | Numeric user and group applied by LinuxServer initialization. In bypass mode, a root `PUID` is automatically replaced at runtime by an unused non-root UID while the configured group is retained. |
| `TZ` | | Optional timezone, for example `Europe/Brussels`. |
| `KEYBOARD` | | Optional Selkies keyboard layout. |
| `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. |
| `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. |
| `permission_mode` | `auto` | Claude Code permission policy: `strict`, `auto`, or `bypass`. |
| `install_headroom` | `true` | Register Headroom MCP and run the supervised local proxy. |
| `headroom_wrap_claude_code` | `true` | Route PATH-based Claude Code launches through the already-running Headroom proxy. |
| `expose_headroom_dashboard` | `false` | Bind Headroom to all interfaces. Port `8787/tcp` must also be mapped manually. |
| `install_rtk` | `true` | Configure RTK's Claude Code `PreToolUse` Bash hook. |
| `install_tokensave` | `true` | Install TokenSave's complete global Claude integration. |
| `tokensave_project_paths` | `[]` | Explicit absolute Git repository paths to initialize or sync at startup. |
| `install_caveman` | `false` | Install the third-party Caveman Claude Code plugin at startup. |
| `enable_tools_health_report` | `true` | Write independent Headroom, RTK, and TokenSave gains to the add-on log hourly. |
| `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. |
| `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_url` | `http://homeassistant:8123/api/mcp` | Streamable HTTP endpoint of Home Assistant's MCP Server integration. |
| `ha_mcp_token` | | Home Assistant long-lived access token used by the MCP bridge. |
| `enable_ha_api_helper` | `true` | Ship the `ha-cli` Core-API helper and add guidance so Claude can configure Home Assistant without a `/config` mount. |
| `additional_apps` | | Comma-separated Debian apt packages to install at startup. |
| `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. |
### Permission modes
```yaml
permission_mode: auto
```
- `strict` keeps Claude Code's normal interactive permission prompts.
- `auto` asks Claude Code's automatic permission classifier to approve safe
operations while retaining prompts for risky actions. This is the default.
- `bypass` disables Claude Code permission checks by using
`bypassPermissions` in the shared settings and
`--dangerously-skip-permissions` for wrapper-launched sessions.
Claude Code does not permit bypass mode when its effective UID is `0`. If the
add-on is configured with `PUID: 0`, selecting `bypass` remaps only the shared
`abc` runtime account to an available non-root UID (preferring `1000`, then
`911`) before storage ownership and Desktop startup. Its configured primary
GID is retained, so group-based access to mounted Home Assistant paths remains
available. Strict and auto modes keep the configured identity unchanged.
A root shell invoking `/usr/local/bin/claude` in bypass mode is also dropped to
the remapped `abc` account. Directly invoking `/usr/bin/claude` as root still
bypasses the add-on wrapper and will be rejected by Claude Code.
`bypass` gives Claude broad authority over all mounted writable data and every
command or credential available inside the add-on. Enable it only in a trusted
installation with trusted repositories and mounts. Mounted paths must remain
accessible to the effective non-root UID or its retained group.
### TokenSave project example
Only repositories listed here are indexed. Paths must be absolute, mounted in
the add-on, and resolve to a Git working tree:
```yaml
tokensave_project_paths:
- /share/projects/hassio-addons
- /share/projects/birdnet-go
```
At startup, an uninitialized repository receives `tokensave init`; an existing
index receives an incremental `tokensave sync`. Removing a path from the option
stops automatic synchronization but does not delete its `.tokensave` database.
Configured repositories are added to Git's `safe.directory` list for the shared
runtime user before TokenSave performs repository discovery.
## Headroom behavior
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.
with the explicit local proxy URL in Claude Desktop and Claude Code, then starts
a supervised Headroom backend on `127.0.0.1:8787`.
Claude Desktop overrides `ANTHROPIC_BASE_URL`, so it is deliberately launched
without proxy injection; the MCP integration is the supported path.
Claude Desktop overrides `ANTHROPIC_BASE_URL`, so Desktop chat deliberately uses
the MCP integration. The `/usr/local/bin/claude` wrapper routes PATH-based Claude
Code sessions through `headroom wrap claude --no-proxy`, reusing the supervised
backend without starting a second proxy.
The Headroom dashboard is available at:
The dashboard is disabled externally by default. To expose it:
```text
http://<home-assistant-host>:8787/dashboard
1. Set `expose_headroom_dashboard: true`.
2. Map `8787/tcp` in the add-on **Network** section.
3. Open `http://<home-assistant-host>:8787/dashboard`.
The dashboard is unauthenticated. Do not publish this port to the public
internet.
## Diagnostics
Run the following inside the add-on through a custom script or container console:
```bash
claude-tools-doctor.sh
```
through the default `8787/tcp` port mapping. Treat this endpoint as sensitive:
it serves your local network only — do not expose it directly to the public
internet, and unmap the port in the add-on **Network** section if you do not
want it reachable at all.
The report checks the tool binaries, configuration switches, configured and
effective runtime identities, redacted MCP registrations, Claude hooks,
permission mode, Headroom health, TokenSave indexes, routing, and recorded
savings. It never prints MCP environment values because the Home Assistant MCP
entry can contain a long-lived token.
The hourly report can also be invoked manually:
```bash
claude-gains-report.sh
```
## Home Assistant MCP bridge
@@ -111,9 +213,48 @@ To let Claude query and control Home Assistant:
3. Set `enable_ha_mcp: true` and paste the token into `ha_mcp_token` in the
add-on configuration, then restart the add-on.
The add-on bridges Claude to the integration's SSE endpoint with `mcp-proxy`.
Override `ha_mcp_url` only if your Home Assistant instance is not reachable as
`homeassistant:8123` from add-ons.
The add-on bridges Claude to the integration's stateless Streamable HTTP
endpoint (`/api/mcp`) with `mcp-proxy`. Override `ha_mcp_url` only if your Home
Assistant instance is not reachable as `homeassistant:8123` from add-ons.
## Configuring Home Assistant (API helper)
When `enable_ha_api_helper` is on (the default), the add-on ships a `ha-cli`
command and tells Claude — via a managed block in `~/.claude/CLAUDE.md` — that
it can configure Home Assistant through the Home Assistant **Core API** rather
than a filesystem mount. This is deliberately more contained than mapping
`/config`: the API cannot read `configuration.yaml`, `secrets.yaml`, or any
other add-on's stored credentials.
`ha-cli` authenticates automatically with the add-on's `SUPERVISOR_TOKEN`
through the Supervisor Core-API proxy (the add-on already sets
`homeassistant_api: true`), so there is nothing to configure. It can create and
edit automations, scripts, and scenes; call any service; read entity states;
and, over WebSocket, manage helpers, dashboards, and the area/label/floor/entity
registries. Run `ha-cli --help` inside the add-on for the full command
reference.
```bash
ha-cli config # connectivity check
ha-cli get config/automation/config/<id> # read one automation
ha-cli post config/automation/config/<id> @new.json # create/update it
ha-cli call automation.reload # apply YAML-mode changes
ha-cli ws '{"type":"config/area_registry/list"}'
```
Security notes:
- The Supervisor proxy token grants **admin-equivalent** Core API access (it can
call any service and edit any UI-managed configuration), but it cannot reach
the raw YAML files or other add-ons' data. For a tighter scope, set
`HA_BASE_URL`/`HA_TOKEN` (or the `ha_mcp_token` option) to a limited Home
Assistant user's long-lived token — `ha-cli` prefers those when present.
- The guidance instructs Claude to read each object and show you the intended
change before writing, but Claude Code's own tool-permission prompts remain
the real gate: each `ha-cli` call still needs your approval unless
`permission_mode` is set to `bypass`.
- Set `enable_ha_api_helper: false` to remove both the guidance block and the
helper's registration if you do not want Claude configuring Home Assistant.
## Custom scripts
@@ -129,9 +270,12 @@ Persistent state is stored in the configured `data_location` (default
- 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`
- Headroom, RTK, and tokensave user state: their standard paths below the
- Claude Code settings, hooks, sessions, plugins, and permission mode:
`~/.claude`
- Headroom, RTK, and TokenSave user state: their standard paths below the
shared home
- TokenSave repository indexes: `.tokensave/` inside each explicitly configured
project
Volatile cache data is redirected to `/tmp/cache` through `$XDG_CACHE_HOME` and
`$HOME/.cache`.

View File

@@ -54,7 +54,7 @@ magic link into the in-session Chromium (not a phone).
- Add-on Configuration → `additional_apps: chromium`, restart (installed by
`rootfs/etc/cont-init.d/80-configuration.sh`).
- Add the two `xdg-settings`/`xdg-mime` commands to the custom script
`/addon_configs/db21ed7f_claude-desktop/claude-desktop.sh` (the image ships no standalone
`/addon_configs/db21ed7f_claude-desktop/claude_desktop.sh` (the image ships no standalone
terminal).
---
@@ -98,14 +98,14 @@ Claude Desktop uses. No extra `dbus-launch` is needed.
then exposes the Secret Service and exports `GNOME_KEYRING_CONTROL`/`SSH_AUTH_SOCK`.
- `--password-store=gnome-libsecret` forces Electron to use the libsecret backend instead
of falling back to plaintext.
3. Persistence: the keyring DB lives in `$HOME/.local/share/keyrings/` and `HOME=/config/data`
3. Persistence: the keyring DB lives in `$HOME/.local/share/keyrings/` and `HOME=/data/data`
(persistent add-on storage), so the empty-password login keyring survives restarts and is
re-unlocked automatically each boot by the same `autostart` line — the sign-in then sticks.
### User-side workaround (no rebuild)
- Add-on Configuration → `additional_apps: gnome-keyring, libsecret-1-0, dbus-x11`, restart.
- Add the keyring-start lines above to the custom script
`/addon_configs/db21ed7f_claude-desktop/claude-desktop.sh`, and relaunch Claude Desktop
`/addon_configs/db21ed7f_claude-desktop/claude_desktop.sh`, and relaunch Claude Desktop
with `--password-store=gnome-libsecret` (e.g. edit the in-session openbox autostart).
---

View File

@@ -2,7 +2,7 @@ arch:
- aarch64
- amd64
audio: true
description: "Claude Desktop with Headroom MCP context compression and RTK acceleration"
description: "Claude Desktop with Headroom, RTK, and TokenSave optimization"
devices:
- /dev/dri
- /dev/dri/card0
@@ -13,8 +13,8 @@ environment:
AUTO_GPU: "1"
FM_HOME: /data/data
HOME: /data/data
PGID: "0"
PUID: "0"
PGID: "1000"
PUID: "1000"
SELKIES_FRAMERATE: "30"
START_DOCKER: "false"
TITLE: Claude Desktop
@@ -35,30 +35,34 @@ options:
env_vars: []
DNS_server: 8.8.8.8
data_location: /data/data
PGID: 0
PUID: 0
additional_apps: ""
additional_pip: ""
auto_update: true
github_email: ""
enable_ha_mcp: false
ha_mcp_url: http://homeassistant:8123/mcp_server/sse
ha_mcp_url: http://homeassistant:8123/api/mcp
ha_mcp_token: ""
enable_ha_api_helper: true
github_token: ""
github_username: ""
install_caveman: true
enable_tools_health_report: true
expose_headroom_dashboard: false
headroom_wrap_claude_code: true
install_caveman: false
install_github_cli: true
install_headroom: true
install_rtk: true
install_tokensave: true
permission_mode: auto
tokensave_project_paths: []
panel_admin: false
panel_icon: mdi:robot-happy
ports:
3001/tcp: null
8787/tcp: 8787
8787/tcp: null
ports_description:
3001/tcp: Claude Desktop web interface
8787/tcp: Headroom dashboard and proxy
8787/tcp: Optional Headroom dashboard and proxy
privileged:
- SYS_ADMIN
- DAC_READ_SEARCH
@@ -71,26 +75,35 @@ schema:
DRINODE: list(/dev/dri/card0|/dev/dri/card1|/dev/dri/card2|/dev/dri/renderD128|/dev/dri/renderD129|)?
KEYBOARD: list(da-dk-qwerty|de-de-qwertz|en-gb-qwerty|en-us-qwerty|es-es-qwerty|fr-ch-qwertz|fr-fr-azerty|it-it-qwerty|ja-jp-qwerty|pt-br-qwerty|sv-se-qwerty|tr-tr-qwerty)?
PASSWORD: str?
PGID: int
PUID: int
TZ: match([A-Z][a-z]*./[A-Z][a-z]*.)?
additional_apps: str?
additional_pip: str?
auto_update: bool?
cifsdomain: str?
cifspassword: str?
cifsusername: str?
localdisks: str?
networkdisks: str?
github_email: str?
enable_ha_mcp: bool?
ha_mcp_url: str?
ha_mcp_token: password?
enable_ha_api_helper: bool?
github_token: password?
github_username: str?
enable_tools_health_report: bool
expose_headroom_dashboard: bool
headroom_wrap_claude_code: bool
install_caveman: bool
install_github_cli: bool
install_headroom: bool
install_rtk: bool
install_tokensave: bool
permission_mode: list(strict|auto|bypass)
tokensave_project_paths:
- str
slug: claude_desktop
tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "1.17"
version: "1.25"
video: true

View File

@@ -1,4 +1,4 @@
# Hourly rtk + headroom token-savings report to the add-on log (heartbeat + gains).
# Hourly RTK + Headroom + TokenSave savings report to the add-on log.
# Seeded to /data/data/crontabs/root by init-crontab-config and run by svc-cron; edit the
# persistent copy to customize. Output goes to /proc/1/fd/1 so it shows in the add-on log.
0 * * * * /usr/local/bin/claude-gains-report.sh > /proc/1/fd/1 2>&1

View File

@@ -3,9 +3,10 @@
# shellcheck disable=SC2046
set -e
# Define user
PUID=$(bashio::config "PUID")
PGID=$(bashio::config "PGID")
# Use the effective shared desktop user identity. In bypass mode an earlier init script may
# remap abc away from UID 0 because Claude Code rejects bypass permissions when run as root.
PUID=1000
PGID=1000
# Check data location
LOCATION="$(bashio::config 'data_location')"
@@ -81,7 +82,7 @@ fi
ln -sfn /tmp/cache "$LOCATION/.cache"
bashio::log.info "Setting ownership to $PUID:$PGID"
chown -R "$PUID":"$PGID" "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR"
chown -R abc "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR" /data
chmod -R 700 "$LOCATION"
# The base init-selkies-config script overrides XDG_RUNTIME_DIR to $HOME/.XDG, which lands

View File

@@ -2,11 +2,9 @@
# shellcheck shell=bash
set -e
if bashio::config.true 'auto_update'; then
bashio::log.info "Checking for Claude Desktop updates..."
if apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null && apt-get install -y --only-upgrade claude-desktop &> /dev/null; then
bashio::log.info "Claude Desktop version: $(dpkg-query -W -f='${Version}' claude-desktop)"
else
bashio::log.warning "Update check failed (offline?), keeping current version"
fi
bashio::log.info "Checking for Claude Desktop updates..."
if apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null && apt-get install -y --only-upgrade claude-desktop &> /dev/null; then
bashio::log.info "Claude Desktop version: $(dpkg-query -W -f='${Version}' claude-desktop)"
else
bashio::log.warning "Update check failed (offline?), keeping current version"
fi

View File

@@ -0,0 +1,36 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
set -o pipefail
if ! bashio::config.true 'install_tokensave' || ! command -v git > /dev/null 2>&1; then
exit 0
fi
declare -A REPOS_SEEN=()
while IFS= read -r configured_path; do
configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}"
configured_path="${configured_path%"${configured_path##*[![:space:]]}"}"
[ -n "$configured_path" ] || continue
case "$configured_path" in
/*) ;;
*) continue ;;
esac
[ -d "$configured_path" ] || continue
# The one-shot safe.directory override is used only to discover the repository root.
# Persist the resolved root in the shared runtime user's Git config before 82-claude_tools.sh
# performs normal repository detection, avoiding Git's dubious-ownership rejection.
repo_root="$(s6-setuidgid abc env HOME="$HOME" \
git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
[ -n "$repo_root" ] && [ "$repo_root" != "/" ] || continue
[[ -z "${REPOS_SEEN[$repo_root]:-}" ]] || continue
REPOS_SEEN[$repo_root]=1
if ! s6-setuidgid abc env HOME="$HOME" git config --global --get-all safe.directory \
| grep -Fxq -- "$repo_root"; then
s6-setuidgid abc env HOME="$HOME" git config --global --add safe.directory "$repo_root"
bashio::log.info "Marked TokenSave repository as safe for Git: ${repo_root}"
fi
done < <(bashio::config.array 'tokensave_project_paths')

View File

@@ -7,15 +7,18 @@ PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else ech
PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '0'; fi)"
mkdir -p "$HOME/.claude"
run_as_runtime_user() {
s6-setuidgid abc env HOME="$HOME" "$@"
}
CLAUDE_DESKTOP_COMMAND_FILE="/tmp/claude-desktop-command"
DEFAULT_CLAUDE_DESKTOP_COMMAND='claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret'
printf '%s\n' "$DEFAULT_CLAUDE_DESKTOP_COMMAND" > "$CLAUDE_DESKTOP_COMMAND_FILE"
# headroom's "wrap"/proxy routing works by setting ANTHROPIC_BASE_URL, which the Claude Desktop
# 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.
# Headroom's proxy routing works by setting ANTHROPIC_BASE_URL, which the Claude Desktop
# Electron app force-overrides to the production endpoint (headroom #869). Desktop therefore
# uses Headroom's MCP tools. Claude Code launches that resolve `claude` through PATH use the
# add-on's /usr/local/bin/claude wrapper and can be transparently proxied when enabled.
#
# 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).
@@ -38,17 +41,25 @@ 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"
bashio::log.info "tokensave $(tokensave --version 2> /dev/null || true) available; configuring the complete Claude Code integration"
# The upstream installer adds the MCP entry, PreToolUse/UserPromptSubmit/Stop hooks,
# MCP permissions, global CLAUDE.md rules, and the global post-commit/checkout sync hook.
run_as_runtime_user tokensave install --agent claude --git-hook yes \
|| bashio::log.warning "tokensave Claude Code integration setup failed"
else
bashio::log.warning "tokensave is not available"
fi
elif command -v tokensave &> /dev/null; then
bashio::log.info "Removing the tokensave Claude Code integration"
run_as_runtime_user tokensave uninstall --agent claude \
|| bashio::log.warning "tokensave Claude Code integration removal failed"
fi
HA_MCP_ENABLED=false
HA_MCP_URL=""
HA_MCP_TOKEN=""
if bashio::config.true 'enable_ha_mcp'; then
HA_MCP_URL="$(bashio::config 'ha_mcp_url' 'http://homeassistant:8123/mcp_server/sse')"
HA_MCP_URL="$(bashio::config 'ha_mcp_url' 'http://homeassistant:8123/api/mcp')"
if bashio::config.has_value 'ha_mcp_token'; then
HA_MCP_TOKEN="$(bashio::config 'ha_mcp_token')"
fi
@@ -63,7 +74,7 @@ if bashio::config.true 'enable_ha_mcp'; then
fi
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="/usr/local/bin/tokensave" \
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \
HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
@@ -72,26 +83,44 @@ import json
import os
from pathlib import Path
MANAGED_COMMANDS = {
"headroom": {os.environ["HEADROOM_BIN"], "headroom"},
"tokensave": {os.environ["TOKENSAVE_BIN"], "tokensave"},
"homeassistant": {os.environ["MCP_PROXY_BIN"], "mcp-proxy"},
MANAGED_BASENAMES = {
"headroom": "headroom",
"tokensave": "tokensave",
"homeassistant": "mcp-proxy",
}
desired = {}
if os.environ["HEADROOM_ENABLED"] == "true":
desired["headroom"] = {"command": os.environ["HEADROOM_BIN"], "args": ["mcp", "serve"]}
desired["headroom"] = {
"command": os.environ["HEADROOM_BIN"],
"args": ["mcp", "serve", "--proxy-url", "http://127.0.0.1:8787"],
}
if os.environ["TOKENSAVE_ENABLED"] == "true":
desired["tokensave"] = {"command": os.environ["TOKENSAVE_BIN"], "args": ["serve"]}
if os.environ["HA_MCP_ENABLED"] == "true":
# Home Assistant's MCP Server integration speaks stateless Streamable HTTP on /api/mcp;
# mcp-proxy defaults to SSE, so the transport flags are required.
desired["homeassistant"] = {
"command": os.environ["MCP_PROXY_BIN"],
"args": [os.environ["HA_MCP_URL"]],
"args": ["--transport=streamablehttp", "--stateless", os.environ["HA_MCP_URL"]],
"env": {"API_ACCESS_TOKEN": os.environ["HA_MCP_TOKEN"]},
}
# An entry is add-on-managed when its command is one of our binaries living outside the
# persistent home. Matching on the basename (rather than the exact path recorded at write
# time) keeps entries updatable when a base-image upgrade moves the binary, while commands
# under $HOME stay untouched because those are user-installed.
HOME_PREFIX = os.path.expanduser("~") + os.sep
def is_managed(name, entry):
return isinstance(entry, dict) and entry.get("command") in MANAGED_COMMANDS[name]
if not isinstance(entry, dict):
return False
command = entry.get("command")
if not isinstance(command, str) or command.startswith(HOME_PREFIX):
return False
return os.path.basename(command) == MANAGED_BASENAMES[name]
for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_CONFIG", True)):
path = Path(os.environ[config_var])
@@ -107,7 +136,7 @@ for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_C
if not isinstance(servers, dict):
servers = {}
changed = False
for name in MANAGED_COMMANDS:
for name in MANAGED_BASENAMES:
existing = servers.get(name)
if name in desired:
entry = dict(desired[name])
@@ -128,14 +157,109 @@ for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_C
data.pop("mcpServers", None)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, indent=2) + "\n")
# The Home Assistant long-lived access token is stored here in clear text.
path.chmod(0o600)
PY
# Guide Claude to actually use the headroom compression tools so the MCP integration produces
# real savings (otherwise the tools sit unused and `headroom savings` stays empty). Managed,
# idempotent block appended to the user's global CLAUDE.md; removed when headroom is disabled.
# Initialize or incrementally sync only explicitly configured repositories. TokenSave deliberately
# requires one-time per-project opt-in; an empty list therefore has no startup or storage cost.
if $TOKENSAVE_ENABLED; then
declare -A TOKENSAVE_REPOS_SEEN=()
while IFS= read -r configured_path; do
# Trim surrounding whitespace while preserving spaces inside paths.
configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}"
configured_path="${configured_path%"${configured_path##*[![:space:]]}"}"
[ -n "$configured_path" ] || continue
case "$configured_path" in
/*) ;;
*)
bashio::log.warning "Skipping non-absolute tokensave_project_paths entry: ${configured_path}"
continue
;;
esac
if [ ! -d "$configured_path" ]; then
bashio::log.warning "Skipping missing TokenSave project path: ${configured_path}"
continue
fi
repo_root="$(git -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
if [ -z "$repo_root" ] || [ "$repo_root" = "/" ]; then
bashio::log.warning "Skipping TokenSave path that is not a supported Git repository: ${configured_path}"
continue
fi
if [[ -n "${TOKENSAVE_REPOS_SEEN[$repo_root]:-}" ]]; then
continue
fi
TOKENSAVE_REPOS_SEEN[$repo_root]=1
bashio::log.info "Preparing TokenSave index: ${repo_root}"
# Prepare the per-repo semantic graph defensively so a hard add-on stop or storage
# hiccup can never leave a broken index that fails every subsequent boot:
# * a startup-scoped flock serializes against an overlapping restart (and any git
# post-commit/checkout sync hook that fires mid-boot), so two writers never race;
# * an existing index is refreshed with a cheap incremental `sync`, retried a few
# times because SQLITE_BUSY under lock contention is transient, not corruption;
# * only a genuinely unreadable/malformed index (sync still failing after retries)
# or a half-written one from an interrupted `init` is quarantined and rebuilt,
# so the graph self-heals instead of propagating corruption;
# * `init` is bracketed by a sentinel file so an interrupted full build is detected
# as incomplete on the next start and rebuilt rather than trusted.
# All file operations run as the abc runtime user because the repo `.tokensave`
# directory is not covered by this script's final ownership pass.
# shellcheck disable=SC2016 # single-quoted on purpose: $1/$db/etc. expand in the abc shell
run_as_runtime_user bash -c '
set -o pipefail
repo_root="$1"
ts_dir="$repo_root/.tokensave"
db="$ts_dir/tokensave.db"
lock="$ts_dir/.startup.lock"
initflag="$ts_dir/.init-incomplete"
mkdir -p "$ts_dir"
exec 9>"$lock"
if ! flock -n 9; then
echo "TokenSave: index busy for $repo_root; skipping startup sync" >&2
exit 0
fi
quarantine() {
stamp="$(date +%Y%m%d-%H%M%S)"
bdir="$ts_dir/corrupt-$stamp"
mkdir -p "$bdir"
for f in "$db" "$db-wal" "$db-shm"; do
[ -e "$f" ] && mv -f "$f" "$bdir/" 2>/dev/null || true
done
echo "TokenSave: quarantined suspect index to $bdir" >&2
}
if [ -f "$db" ] && [ ! -f "$initflag" ]; then
attempt=1
while :; do
tokensave sync "$repo_root" && exit 0
[ "$attempt" -ge 3 ] && break
echo "TokenSave: sync attempt $attempt failed for $repo_root; retrying" >&2
attempt=$((attempt + 1))
sleep 2
done
echo "TokenSave: sync failed after retries for $repo_root; rebuilding index" >&2
quarantine
elif [ -f "$db" ]; then
echo "TokenSave: previous init did not finish for $repo_root; rebuilding index" >&2
quarantine
fi
: > "$initflag"
tokensave init "$repo_root" && { rm -f "$initflag"; exit 0; }
echo "TokenSave: init failed for $repo_root; will retry on next start" >&2
exit 1
' _ "$repo_root" \
|| bashio::log.warning "TokenSave preparation failed for ${repo_root}"
done < <(bashio::config.array 'tokensave_project_paths')
fi
# Guide Claude to actually use the Headroom compression tools so the MCP integration produces
# real savings when transparent proxying is unavailable. Managed, idempotent block appended to
# the user's global CLAUDE.md; removed when Headroom is disabled.
CLAUDE_MD="$HOME/.claude/CLAUDE.md"
HEADROOM_GUIDE_BEGIN="<!-- BEGIN headroom (managed by claude_desktop addon) -->"
if bashio::config.true 'install_headroom'; then
if $HEADROOM_ENABLED; then
mkdir -p "$(dirname "$CLAUDE_MD")"
if ! { [ -f "$CLAUDE_MD" ] && grep -qF "$HEADROOM_GUIDE_BEGIN" "$CLAUDE_MD"; }; then
bashio::log.info "Adding headroom usage guidance to CLAUDE.md"
@@ -176,16 +300,70 @@ if new != text:
PY
fi
# Tell Claude Code that it can configure Home Assistant over the Core API via the shipped
# `ha-cli` helper (no /config filesystem mount needed). Managed, idempotent block appended to
# the user's global CLAUDE.md; removed when the helper is disabled. Mirrors the headroom block.
HA_HELPER_GUIDE_BEGIN="<!-- BEGIN ha-api-helper (managed by claude_desktop addon) -->"
if bashio::config.true 'enable_ha_api_helper'; then
mkdir -p "$(dirname "$CLAUDE_MD")"
if ! { [ -f "$CLAUDE_MD" ] && grep -qF "$HA_HELPER_GUIDE_BEGIN" "$CLAUDE_MD"; }; then
bashio::log.info "Adding Home Assistant API helper guidance to CLAUDE.md"
{
[ -s "$CLAUDE_MD" ] && printf '\n'
cat <<'MD'
<!-- BEGIN ha-api-helper (managed by claude_desktop addon) -->
## Configuring Home Assistant
You can configure this Home Assistant instance through its Core API using the `ha-cli`
command (on `PATH`). It authenticates automatically with the add-on's `$SUPERVISOR_TOKEN`,
so no token setup is needed. There is **no `/config` filesystem mount** — work only through
`ha-cli`, and never try to read or write Home Assistant YAML files directly.
What is editable this way: automations, scripts, and scenes
(`ha-cli get|post|delete config/automation/config/<id>` and the `script`/`scene` equivalents);
service calls (`ha-cli call <domain.service> '<json>'`); state reads (`ha-cli states`); and,
over WebSocket, helpers, dashboards, and area/label/floor/entity registries
(`ha-cli ws '{"type":"..."}'`). Run `ha-cli --help` for the full reference. Raw YAML
(`configuration.yaml`, `secrets.yaml`) is intentionally unreachable — if a change needs it,
say so instead of working around it.
Rules: run `ha-cli config` first to confirm connectivity; **read the current object and show
the user the intended change, then wait for confirmation** before any create/update/delete or
any state-changing `call`; after writing, read the object back and reload if needed
(e.g. `ha-cli call automation.reload`).
<!-- END ha-api-helper (managed by claude_desktop addon) -->
MD
} >> "$CLAUDE_MD"
fi
elif [ -f "$CLAUDE_MD" ] && grep -qF "$HA_HELPER_GUIDE_BEGIN" "$CLAUDE_MD"; then
bashio::log.info "Removing Home Assistant API helper guidance from CLAUDE.md"
CLAUDE_MD="$CLAUDE_MD" python3 - <<'PY' || bashio::log.warning "Unable to remove Home Assistant API helper guidance automatically"
import os
import re
from pathlib import Path
path = Path(os.environ["CLAUDE_MD"])
text = path.read_text(encoding="utf-8")
pattern = re.compile(
r"\n*<!-- BEGIN ha-api-helper \(managed by claude_desktop addon\) -->.*?"
r"<!-- END ha-api-helper \(managed by claude_desktop addon\) -->\n?",
re.DOTALL,
)
new = pattern.sub("", text)
if new != text:
path.write_text(new, encoding="utf-8")
PY
fi
if bashio::config.true 'install_rtk'; then
if command -v rtk &> /dev/null; then
if [ -f "$HOME/.claude/settings.json" ] && grep -q 'rtk hook claude' "$HOME/.claude/settings.json"; then
bashio::log.info "rtk Claude Code hook already configured"
else
bashio::log.info "Configuring rtk Claude Code hook"
RTK_NONINTERACTIVE=1 rtk init -g || bashio::log.warning "rtk global files configuration failed"
python3 - <<'PY' || bashio::log.warning "Unable to configure rtk hook automatically"
bashio::log.info "Configuring rtk Claude Code integration"
run_as_runtime_user env RTK_NONINTERACTIVE=1 rtk init -g \
|| bashio::log.warning "rtk global files configuration failed"
python3 - <<'PY' || bashio::log.warning "Unable to configure rtk hook automatically"
import json
from pathlib import Path
path = Path.home() / ".claude" / "settings.json"
try:
data = json.loads(path.read_text()) if path.exists() else {}
@@ -203,7 +381,6 @@ if not any("rtk hook claude" in json.dumps(entry) for entry in pre if isinstance
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, indent=2) + "\n")
PY
fi
else
bashio::log.warning "rtk is not available"
fi
@@ -275,7 +452,8 @@ if bashio::config.true 'install_caveman'; then
bashio::log.info "caveman Claude Code plugin already configured"
else
bashio::log.info "Installing caveman Claude Code plugin"
curl --connect-timeout 10 --max-time 60 -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash >/dev/null || bashio::log.warning "caveman install failed (offline?)"
curl --connect-timeout 10 --max-time 60 -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash > /dev/null \
|| bashio::log.warning "caveman install failed (offline?)"
fi
else
bashio::log.info "Disabling caveman Claude Code plugin"

View File

@@ -0,0 +1,92 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
set -o pipefail
PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else echo '0'; fi)"
PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '0'; fi)"
PERMISSION_MODE="$(bashio::config 'permission_mode')"
SETTINGS_PATH="$HOME/.claude/settings.json"
STATE_PATH="$HOME/.claude/.addon-permission-mode.json"
case "$PERMISSION_MODE" in
strict|auto|bypass) ;;
*)
bashio::log.warning "Unknown permission_mode '${PERMISSION_MODE}'; falling back to strict"
PERMISSION_MODE="strict"
;;
esac
mkdir -p "$(dirname "$SETTINGS_PATH")"
PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" STATE_PATH="$STATE_PATH" python3 - <<'PY'
import json
import os
from pathlib import Path
mode = os.environ["PERMISSION_MODE"]
settings_path = Path(os.environ["SETTINGS_PATH"])
state_path = Path(os.environ["STATE_PATH"])
try:
settings = json.loads(settings_path.read_text()) if settings_path.exists() else {}
except (OSError, json.JSONDecodeError):
if settings_path.exists():
settings_path.rename(settings_path.with_suffix(settings_path.suffix + ".bak"))
settings = {}
if not isinstance(settings, dict):
settings = {}
try:
state = json.loads(state_path.read_text()) if state_path.exists() else None
except (OSError, json.JSONDecodeError):
state = None
if not isinstance(state, dict):
state = None
permissions = settings.get("permissions")
if not isinstance(permissions, dict):
permissions = {}
if mode == "strict":
# Restore the value that existed before the add-on first managed this setting.
if state is not None:
if state.get("previous_exists"):
permissions["defaultMode"] = state.get("previous_value")
else:
permissions.pop("defaultMode", None)
state_path.unlink(missing_ok=True)
else:
if state is None:
state = {
"previous_exists": "defaultMode" in permissions,
"previous_value": permissions.get("defaultMode"),
}
state_path.write_text(json.dumps(state, indent=2) + "\n")
state_path.chmod(0o600)
permissions["defaultMode"] = "auto" if mode == "auto" else "bypassPermissions"
if permissions:
settings["permissions"] = permissions
else:
settings.pop("permissions", None)
settings_path.write_text(json.dumps(settings, indent=2) + "\n")
settings_path.chmod(0o600)
PY
case "$PERMISSION_MODE" in
strict)
bashio::log.info "Claude Code permission mode: strict (normal prompts)"
;;
auto)
bashio::log.info "Claude Code permission mode: auto (safe actions approved automatically)"
;;
bypass)
bashio::log.warning "Claude Code permission mode: bypass (permission checks disabled for mounted data and available tools)"
;;
esac
chown -- "${PUID}:${PGID}" "$SETTINGS_PATH" 2> /dev/null || true
if [ -e "$STATE_PATH" ]; then
chown -- "${PUID}:${PGID}" "$STATE_PATH" 2> /dev/null || true
fi

View File

@@ -0,0 +1,17 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# Earlier configuration scripts intentionally run as root and may use the configured PUID/PGID
# values when returning files to the runtime user. In bypass mode PUID can still be configured as
# 0 even though 19-claude_bypass_runtime.sh remapped abc to a non-root UID. Reconcile ownership
# with the effective desktop identity after all Claude configuration writes are complete.
RUNTIME_UID="$(id -u abc)"
RUNTIME_GID="$(id -g abc)"
for managed_path in "$HOME/.claude" "$HOME/.claude.json" "$HOME/.config/Claude"; do
if [ -e "$managed_path" ]; then
chown -R -- "${RUNTIME_UID}:${RUNTIME_GID}" "$managed_path" \
|| bashio::log.warning "Unable to set effective runtime ownership on $managed_path"
fi
done

View File

@@ -1,11 +1,15 @@
#!/usr/bin/with-contenv bashio
# Headroom optimization proxy — local backend for Claude Desktop MCP and Claude Code.
declare port=8787
# 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
declare host=127.0.0.1
if bashio::config.true 'install_headroom' && command -v headroom >/dev/null 2>&1; then
# The dashboard is unauthenticated. Keep it container-local by default and bind all
# interfaces only when the user explicitly opts in and maps port 8787.
if bashio::config.true 'expose_headroom_dashboard'; then
host=0.0.0.0
fi
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}"
exec s6-setuidgid abc headroom proxy --host "${host}" --port "${port}" --code-aware
fi

View File

@@ -0,0 +1,53 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -o pipefail
REAL_CLAUDE="/usr/bin/claude"
HEADROOM_BIN="$(command -v headroom || true)"
HEADROOM_URL="http://127.0.0.1:8787"
PERMISSION_MODE="$(bashio::config 'permission_mode')"
declare -a CLAUDE_PERMISSION_ARGS=()
case "$PERMISSION_MODE" in
bypass)
CLAUDE_PERMISSION_ARGS+=("--dangerously-skip-permissions")
;;
auto)
CLAUDE_PERMISSION_ARGS+=("--permission-mode" "auto")
;;
strict|"")
;;
*)
echo "claude wrapper: unknown permission_mode '${PERMISSION_MODE}', using strict mode" >&2
;;
esac
if [ ! -x "$REAL_CLAUDE" ]; then
echo "claude wrapper: ${REAL_CLAUDE} is unavailable" >&2
exit 127
fi
# Claude Code rejects bypass mode when the effective UID is 0. Normal Desktop sessions run
# as abc, which startup remaps to a non-root UID when bypass is selected. Also handle a user
# invoking this wrapper directly from a root container console by dropping to abc here.
if [ "$PERMISSION_MODE" = "bypass" ] && [ "$(id -u)" -eq 0 ]; then
if command -v s6-setuidgid > /dev/null 2>&1 && [ "$(id -u abc)" -ne 0 ]; then
exec s6-setuidgid abc "$0" "$@"
fi
echo "claude wrapper: bypass mode requires a non-root runtime user, but abc is still UID 0" >&2
exit 1
fi
if bashio::config.true 'install_headroom' && bashio::config.true 'headroom_wrap_claude_code'; then
if [ -x "$HEADROOM_BIN" ] && curl -fsS --max-time 2 "${HEADROOM_URL}/health" > /dev/null 2>&1; then
# Put /usr/bin before /usr/local/bin while Headroom resolves its upstream `claude`
# executable; otherwise it would resolve this wrapper recursively.
export HEADROOM_CONTEXT_TOOL="rtk"
exec env PATH="/usr/bin:/bin:/usr/local/bin" \
"$HEADROOM_BIN" wrap claude --no-proxy -- \
"${CLAUDE_PERMISSION_ARGS[@]}" "$@"
fi
echo "claude wrapper: Headroom proxy is unavailable; launching Claude Code directly" >&2
fi
exec "$REAL_CLAUDE" "${CLAUDE_PERMISSION_ARGS[@]}" "$@"

View File

@@ -1,34 +1,44 @@
#!/usr/bin/with-contenv bashio
# Hourly rtk + headroom token-savings snapshot for the add-on log.
# Invoked by cron (see /defaults/crontabs/root); its stdout is redirected to /proc/1/fd/1,
# so the report appears in the add-on log. Doubles as a heartbeat: if the numbers stop
# growing, the corresponding tool has stopped working.
# with-contenv supplies HOME from the s6 envdir, so this honors a custom `data_location`
# (see 20-folders.sh) instead of hardcoding /data/data; it also makes bashio::config
# available for the install_headroom gate below.
export NO_COLOR=1 # keep the add-on log free of ANSI color codes
# Hourly RTK + Headroom + TokenSave token-savings snapshot for the add-on log.
# Invoked by cron (see /defaults/crontabs/root); stdout is redirected to /proc/1/fd/1.
# Each tool is reported independently so enabling Headroom cannot hide RTK or TokenSave data.
# with-contenv supplies the configured persistent HOME.
export NO_COLOR=1
export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
have_rtk=false; command -v rtk >/dev/null 2>&1 && have_rtk=true
have_headroom=false; command -v headroom >/dev/null 2>&1 && have_headroom=true
# headroom is pip-installed unconditionally at build time, so its binary is on PATH even
# when install_headroom is off — gate on the same config svc-headroom checks, and only
# fall back to have_headroom as a secondary availability guard.
headroom_enabled=false
if bashio::config.true 'install_headroom' && $have_headroom; then
headroom_enabled=true
if ! bashio::config.true 'enable_tools_health_report'; then
exit 0
fi
# Nothing to report if neither tool is active — stay quiet.
if ! $have_rtk && ! $headroom_enabled; then exit 0; fi
rtk_enabled=false
headroom_enabled=false
tokensave_enabled=false
echo "===== claude gains report $(date '+%Y-%m-%d %H:%M:%S') ====="
if bashio::config.true 'install_rtk' && command -v rtk > /dev/null 2>&1; then
rtk_enabled=true
fi
if bashio::config.true 'install_headroom' && command -v headroom > /dev/null 2>&1; then
headroom_enabled=true
fi
if bashio::config.true 'install_tokensave' && command -v tokensave > /dev/null 2>&1; then
tokensave_enabled=true
fi
if ! $rtk_enabled && ! $headroom_enabled && ! $tokensave_enabled; then
exit 0
fi
echo "===== claude tools report $(date '+%Y-%m-%d %H:%M:%S') ====="
if $headroom_enabled; then
echo "--- headroom savings ---"
headroom savings 2>&1 || echo "[warn] headroom savings failed"
elif $have_rtk; then
fi
if $rtk_enabled; then
echo "--- rtk gain ---"
rtk gain 2>&1 || echo "[warn] rtk gain failed"
fi
echo "===== end gains report ====="
if $tokensave_enabled; then
echo "--- tokensave gain ---"
tokensave gain --all --range 30d 2>&1 || echo "[warn] tokensave gain failed"
fi
echo "===== end claude tools report ====="

View File

@@ -0,0 +1,174 @@
#!/usr/bin/with-contenv bashio
# Diagnose installation, registration, routing, indexing, permissions, and recorded savings without
# printing MCP environment values (which may contain the Home Assistant access token).
# shellcheck shell=bash
set +e
set -o pipefail
export NO_COLOR=1
export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
section() {
printf '\n=== %s ===\n' "$1"
}
section "Installed binaries"
for tool in claude claude-desktop headroom rtk tokensave git gh rg jq shellcheck yamllint hadolint actionlint; do
resolved="$(command -v "$tool" 2> /dev/null || true)"
if [ -n "$resolved" ]; then
printf '%-16s %s\n' "$tool" "$resolved"
else
printf '%-16s %s\n' "$tool" "MISSING"
fi
done
section "Configured switches"
for option in permission_mode install_headroom headroom_wrap_claude_code expose_headroom_dashboard install_rtk install_tokensave install_caveman enable_tools_health_report; do
printf '%-30s %s\n' "$option" "$(bashio::config "$option")"
done
section "Runtime identity"
printf '%-30s %s\n' "configured PUID:PGID" "$(bashio::config 'PUID'):$(bashio::config 'PGID')"
printf '%-30s %s\n' "effective abc UID:GID" "$(id -u abc):$(id -g abc)"
printf '%-30s %s\n' "current process UID:GID" "$(id -u):$(id -g)"
if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
if [ "$(id -u abc)" -eq 0 ]; then
echo "bypass runtime: ERROR - Claude Code will reject bypass permissions while abc is root"
else
echo "bypass runtime: OK - Claude Desktop and Cowork run as a non-root UID"
fi
fi
section "Claude Code permission state"
python3 - <<'PY'
import json
from pathlib import Path
path = Path.home() / ".claude/settings.json"
try:
data = json.loads(path.read_text())
except FileNotFoundError:
print("settings: MISSING")
except Exception as exc:
print(f"settings: INVALID: {exc}")
else:
permissions = data.get("permissions", {})
if isinstance(permissions, dict):
print(f"permissions.defaultMode: {permissions.get('defaultMode', '<upstream default>')}")
else:
print("permissions: INVALID")
print(f"managed-state marker: {(Path.home() / '.claude/.addon-permission-mode.json').exists()}")
PY
section "MCP registrations (environment values redacted)"
python3 - <<'PY'
import json
from pathlib import Path
paths = [
Path.home() / ".claude.json",
Path.home() / ".config/Claude/claude_desktop_config.json",
]
for path in paths:
print(path)
try:
data = json.loads(path.read_text())
except FileNotFoundError:
print(" MISSING")
continue
except Exception as exc:
print(f" INVALID: {exc}")
continue
servers = data.get("mcpServers", {})
if not isinstance(servers, dict) or not servers:
print(" no MCP servers")
continue
for name, spec in sorted(servers.items()):
if not isinstance(spec, dict):
print(f" {name}: invalid entry")
continue
command = spec.get("command", "?")
args = spec.get("args", [])
server_type = spec.get("type", "")
suffix = f" type={server_type}" if server_type else ""
print(f" {name}: {command} {args}{suffix}")
if spec.get("env"):
print(" env: <redacted>")
PY
section "Claude Code hooks"
python3 - <<'PY'
import json
from pathlib import Path
path = Path.home() / ".claude/settings.json"
try:
data = json.loads(path.read_text())
except FileNotFoundError:
print("MISSING")
raise SystemExit(0)
except Exception as exc:
print(f"INVALID: {exc}")
raise SystemExit(0)
hooks = data.get("hooks", {})
if not isinstance(hooks, dict) or not hooks:
print("no hooks")
raise SystemExit(0)
for event, entries in hooks.items():
print(event)
if not isinstance(entries, list):
print(" invalid entries")
continue
for entry in entries:
matcher = entry.get("matcher", "*") if isinstance(entry, dict) else "?"
commands = entry.get("hooks", []) if isinstance(entry, dict) else []
rendered = []
for command in commands if isinstance(commands, list) else []:
if isinstance(command, dict):
rendered.append(" ".join([str(command.get("command", "?")), *map(str, command.get("args", []))]))
print(f" matcher={matcher}: {', '.join(rendered) or 'no command'}")
PY
section "Headroom"
if bashio::config.true 'install_headroom'; then
curl -fsS --max-time 2 http://127.0.0.1:8787/health && echo || echo "proxy health: FAILED"
headroom mcp status || true
headroom savings || true
else
echo "disabled"
fi
section "RTK"
if bashio::config.true 'install_rtk'; then
rtk gain || true
else
echo "disabled"
fi
section "TokenSave"
if bashio::config.true 'install_tokensave'; then
tokensave doctor --agent claude || true
tokensave gain --all --range 30d || true
while IFS= read -r configured_path; do
[ -n "$configured_path" ] || continue
repo_root="$(s6-setuidgid abc env HOME="$HOME" git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
if [ -z "$repo_root" ]; then
echo "${configured_path}: not a Git repository"
elif [ -f "$repo_root/.tokensave/tokensave.db" ]; then
s6-setuidgid abc env HOME="$HOME" tokensave status "$repo_root" --short || true
else
echo "${repo_root}: NOT INITIALIZED"
fi
done < <(bashio::config.array 'tokensave_project_paths')
else
echo "disabled"
fi
section "Claude routing"
printf 'PATH claude: %s\n' "$(command -v claude 2> /dev/null || true)"
printf 'real claude: %s\n' "$([ -x /usr/bin/claude ] && echo /usr/bin/claude || echo MISSING)"
if bashio::config.true 'headroom_wrap_claude_code'; then
echo "PATH-based Claude Code launches are configured for Headroom wrapping."
else
echo "Claude Code Headroom wrapping is disabled; Headroom remains available through MCP."
fi

View File

@@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""ha-cli — talk to the local Home Assistant Core API from inside the add-on.
Lets Claude Code configure Home Assistant (automations, scripts, scenes,
helpers, dashboards, areas/labels, service calls) through the API, without any
`/config` filesystem mount. `secrets.yaml` and other add-ons' credentials are
therefore never reachable.
Authentication and the base URL are resolved automatically, in this order:
1. $HA_BASE_URL + $HA_TOKEN explicit override (advanced/scoped)
2. `ha_mcp_token` in /data/options.json scoped user long-lived token -> :8123
3. $SUPERVISOR_TOKEN Supervisor Core-API proxy fallback
(admin-equivalent; needs
homeassistant_api: true, which this
add-on sets — zero setup)
The scoped token is checked before the Supervisor fallback so setting
`ha_mcp_token` actually narrows access instead of being shadowed by the
always-present admin-equivalent Supervisor token.
Subcommands:
ha-cli get <path> GET e.g. get config/automation/config/1700000000
ha-cli post <path> [BODY] POST BODY = inline JSON, @file, or - (stdin)
ha-cli delete <path> DELETE
ha-cli call <domain.service> [BODY] call a service (BODY = JSON service data)
ha-cli states [entity_id] all states, or one entity
ha-cli config GET /config (sanity check / core info)
ha-cli ws <BODY> one WebSocket command (BODY = JSON, @file, or -)
`<path>` is relative to the REST API root; a leading slash and/or `api/` prefix
are optional. Responses are printed as formatted JSON. Exit code is non-zero on
HTTP or API errors.
Use the WebSocket subcommand for things the REST API does not expose:
ha-cli ws '{"type":"config/area_registry/list"}'
ha-cli ws '{"type":"input_boolean/create","name":"Guest mode","icon":"mdi:account"}'
ha-cli ws '{"type":"lovelace/config","url_path":null}'
"""
import json
import os
import sys
import urllib.error
import urllib.request
def _load_option(name):
"""Read a single option from the add-on's /data/options.json, if present."""
try:
with open("/data/options.json", encoding="utf-8") as handle:
return json.load(handle).get(name)
except (OSError, ValueError):
return None
def _helper_enabled():
"""Mirror config.yaml's enable_ha_api_helper default (true) when unset."""
value = _load_option("enable_ha_api_helper")
return value is not False
def resolve_endpoint():
"""Return (rest_base, ws_url, token) for the best available auth path."""
base = os.environ.get("HA_BASE_URL")
token = os.environ.get("HA_TOKEN")
if base and token:
rest = base.rstrip("/")
if not rest.endswith("/api"):
rest += "/api"
ws = rest.replace("http", "ws", 1).rsplit("/api", 1)[0] + "/api/websocket"
return rest, ws, token
# Checked before SUPERVISOR_TOKEN: this add-on always sets homeassistant_api,
# so the admin-equivalent Supervisor token is otherwise always present and
# would shadow a user's deliberately scoped-down ha_mcp_token.
token = _load_option("ha_mcp_token")
if token:
return (
"http://homeassistant:8123/api",
"ws://homeassistant:8123/api/websocket",
token,
)
token = os.environ.get("SUPERVISOR_TOKEN")
if token:
return "http://supervisor/core/api", "ws://supervisor/core/websocket", token
sys.exit(
"ha-cli: no credentials. Expected ha_mcp_token in the add-on options "
"(scoped user), $SUPERVISOR_TOKEN (admin-equivalent fallback, default "
"inside the add-on), or an explicit $HA_BASE_URL+$HA_TOKEN override."
)
def _url(base, path):
path = path.lstrip("/")
if path.startswith("api/"):
path = path[len("api/"):]
return base.rstrip("/") + "/" + path
def _read_body(arg):
"""Resolve an inline-JSON / @file / - (stdin) body argument to a dict/list."""
if arg is None:
return None
if arg == "-":
raw = sys.stdin.read()
elif arg.startswith("@"):
with open(arg[1:], encoding="utf-8") as handle:
raw = handle.read()
else:
raw = arg
raw = raw.strip()
if not raw:
return None
try:
return json.loads(raw)
except ValueError as exc:
sys.exit(f"ha-cli: body is not valid JSON: {exc}")
def _print(obj):
if isinstance(obj, (dict, list)):
print(json.dumps(obj, indent=2, ensure_ascii=False))
elif obj not in (None, ""):
print(obj)
def rest(method, base, token, path, body=None):
data = None
headers = {"Authorization": f"Bearer {token}"}
if body is not None:
data = json.dumps(body).encode("utf-8")
headers["Content-Type"] = "application/json"
req = urllib.request.Request(_url(base, path), data=data, method=method, headers=headers)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
text = resp.read().decode("utf-8")
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", "replace").strip()
sys.exit(f"ha-cli: HTTP {exc.code} {exc.reason} on {method} {path}\n{detail}")
except urllib.error.URLError as exc:
sys.exit(f"ha-cli: cannot reach Home Assistant ({exc.reason}) on {method} {path}")
try:
return json.loads(text) if text.strip() else None
except ValueError:
return text
def ws_command(ws_url, token, command):
try:
import asyncio
import websockets
except ImportError:
sys.exit(
"ha-cli: the 'websockets' Python package is required for the ws "
"subcommand. REST subcommands work without it."
)
async def run():
async with websockets.connect(ws_url, max_size=None) as sock:
hello = json.loads(await sock.recv())
if hello.get("type") != "auth_required":
raise SystemExit(f"ha-cli: unexpected WS greeting: {hello}")
await sock.send(json.dumps({"type": "auth", "access_token": token}))
if json.loads(await sock.recv()).get("type") != "auth_ok":
raise SystemExit("ha-cli: WebSocket authentication failed")
payload = dict(command)
payload["id"] = 1
await sock.send(json.dumps(payload))
while True:
msg = json.loads(await sock.recv())
if msg.get("id") == 1 and msg.get("type") == "result":
return msg
result = asyncio.run(run())
if not result.get("success", True):
_print(result.get("error", result))
sys.exit(1)
return result.get("result", result)
def main(argv):
if not argv or argv[0] in ("-h", "--help", "help"):
print(__doc__)
return 0
if not _helper_enabled():
sys.exit(
"ha-cli: disabled (enable_ha_api_helper is false in the add-on "
"options). Enable it there to let Claude configure Home Assistant."
)
rest_base, ws_url, token = resolve_endpoint()
cmd, args = argv[0], argv[1:]
if cmd == "get":
if len(args) != 1:
sys.exit("usage: ha-cli get <path>")
_print(rest("GET", rest_base, token, args[0]))
elif cmd == "post":
if not args:
sys.exit("usage: ha-cli post <path> [BODY]")
body = _read_body(args[1]) if len(args) > 1 else None
_print(rest("POST", rest_base, token, args[0], body))
elif cmd == "delete":
if len(args) != 1:
sys.exit("usage: ha-cli delete <path>")
_print(rest("DELETE", rest_base, token, args[0]))
elif cmd == "call":
if not args or "." not in args[0]:
sys.exit("usage: ha-cli call <domain.service> [BODY]")
domain, service = args[0].split(".", 1)
body = _read_body(args[1]) if len(args) > 1 else None
_print(rest("POST", rest_base, token, f"services/{domain}/{service}", body or {}))
elif cmd == "states":
path = f"states/{args[0]}" if args else "states"
_print(rest("GET", rest_base, token, path))
elif cmd == "config":
_print(rest("GET", rest_base, token, "config"))
elif cmd == "ws":
if len(args) != 1:
sys.exit("usage: ha-cli ws <BODY>")
command = _read_body(args[0])
if not isinstance(command, dict) or "type" not in command:
sys.exit('ha-cli: ws BODY must be a JSON object with a "type" field')
_print(ws_command(ws_url, token, command))
else:
sys.exit(f"ha-cli: unknown subcommand '{cmd}' (try: ha-cli --help)")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))

View File

@@ -1,3 +1,5 @@
## 1.27 (15-07-2026)
- Fix the Gitea add-on HEALTHCHECK to work correctly when SSL is enabled, so Home Assistant can accurately report the add-on's health status regardless of whether the instance uses HTTP or HTTPS.
## 1.26.4 (2026-06-23)
- Update to latest version from go-gitea/gitea (changelog : https://github.com/go-gitea/gitea/releases)

View File

@@ -133,4 +133,4 @@ HEALTHCHECK \
--retries=5 \
--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
CMD curl -A "HealthCheck: Docker/1.0" -s -f -k --http1.1 "$(cat /run/health_protocol 2>/dev/null || echo http)://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" &>/dev/null || exit 1

View File

@@ -97,5 +97,5 @@ schema:
slug: gitea
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/gitea
version: "1.26.4"
version: "1.27"
webui: "[PROTO:ssl]://[HOST]:[PORT:3000]"

View File

@@ -55,6 +55,7 @@ for file in /config/app.ini /etc/templates/app.ini; do
PROTOCOL=http
sed -i "/server/a PROTOCOL=http" "$file"
fi
echo -n "${PROTOCOL}" > /run/health_protocol
##################
# ADAPT ROOT_URL #

View File

@@ -1,4 +1,5 @@
## &#9888; Open Issue : [🐛 [qBittorrent] Cant update to 5.2.3 (opened 2026-07-09)](https://github.com/alexbelgium/hassio-addons/issues/2836) by [@tschoehuijs](https://github.com/tschoehuijs)
## &#9888; Open Request : [✨ [REQUEST] transmission, document where torrent files are stored (opened 2026-07-14)](https://github.com/alexbelgium/hassio-addons/issues/2852) by [@bilogic](https://github.com/bilogic)
# Hass.io Add-ons: Tor with bridges
[![Donate][donation-badge]](https://www.buymeacoffee.com/alexbelgium)