Compare commits

..

1 Commits

Author SHA1 Message Date
Alexandre
665fcbedd6 Remove temporary Claude build diagnostic 2026-07-10 15:11:48 +02:00
267 changed files with 954 additions and 3203 deletions

View File

@@ -162,7 +162,6 @@ DARKNAGAN,France
DDanii,
DMurzNN,
DUC750,
DY-hub,
DaFlowah,
DaJonas94,
Daafip,Netherlands
@@ -239,7 +238,6 @@ EtienneMD,
Evel270,
Everestlion,
EvertJob,
Exlatis,
Extrunder,
F0264,
F4bsi,Germany
@@ -565,7 +563,6 @@ PhoenixTwoFive,Germany
PhysShell,
PierreNa,France
PietroSpina,
Pingmin,
PiotrKrzyzek,United States
PiratesGhost,
Pixelzeus,
@@ -900,12 +897,10 @@ antorimba,
antx-code,
anyezhe,
aorosora,
araminimichael,
arbal,United States
ardemk,
ared469,
arethefreshest,
arozoire,
arpit-mehra,
artemave,France
artemdanielov,
@@ -1161,7 +1156,6 @@ danbruno,
danctrl,Germany
danez,United States
danieldotnl,Netherlands
danishru,
dannybeeckman,
dannybloomfield,United States
danveitch76,
@@ -1750,7 +1744,6 @@ ljsquare,
llabourdeth,
llewy,
llfjahn,
llugo,
lmalmoreno,Brazil
lnrdmx,
loc4t3llix,
@@ -2057,7 +2050,6 @@ pedrolicassali,
pedromfa,
pedrware,Portugal
peeetek,
peggleg,
pejannl,
pem884,United States
pepelatc,
@@ -2281,7 +2273,6 @@ skalingclouds,United States
skamaleo,
skavieller,
skipper00,
skoducks,United States
skylidefr,
skynet-network,
slimehands,
@@ -2541,7 +2532,6 @@ williamcorsel,Netherlands
willigenburggihaux,
willnewcombe,United Kingdom
wimb0,
wingerasc,
witold-gren,Poland
wonderfulhuber,
wonkygecko,United States
1 username country
162 DDanii
163 DMurzNN
164 DUC750
DY-hub
165 DaFlowah
166 DaJonas94
167 Daafip Netherlands
238 Evel270
239 Everestlion
240 EvertJob
Exlatis
241 Extrunder
242 F0264
243 F4bsi Germany
563 PhysShell
564 PierreNa France
565 PietroSpina
Pingmin
566 PiotrKrzyzek United States
567 PiratesGhost
568 Pixelzeus
897 antx-code
898 anyezhe
899 aorosora
araminimichael
900 arbal United States
901 ardemk
902 ared469
903 arethefreshest
arozoire
904 arpit-mehra
905 artemave France
906 artemdanielov
1156 danctrl Germany
1157 danez United States
1158 danieldotnl Netherlands
danishru
1159 dannybeeckman
1160 dannybloomfield United States
1161 danveitch76
1744 llabourdeth
1745 llewy
1746 llfjahn
llugo
1747 lmalmoreno Brazil
1748 lnrdmx
1749 loc4t3llix
2050 pedromfa
2051 pedrware Portugal
2052 peeetek
peggleg
2053 pejannl
2054 pem884 United States
2055 pepelatc
2273 skamaleo
2274 skavieller
2275 skipper00
skoducks United States
2276 skylidefr
2277 skynet-network
2278 slimehands
2532 willigenburggihaux
2533 willnewcombe United Kingdom
2534 wimb0
wingerasc
2535 witold-gren Poland
2536 wonderfulhuber
2537 wonkygecko United States

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 63 KiB

BIN
.github/stats.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -35,7 +35,6 @@ jobs:
git fetch origin "${{ github.event.before }}" || true
changed_changelog_files=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" | grep -iE '^([^/]+/)?changelog\.(md|txt|ya?ml|json)$' || true)
echo "$changed_changelog_files"
echo "changelogs_files=$changed_changelog_files" >> "$GITHUB_OUTPUT"
changed_config_files=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" | grep -E '^[^/]+/config\.(json|ya?ml)$' || true)
echo "$changed_config_files"
all_changed_files=$(echo -e "$changed_config_files\n$changed_changelog_files" | sort -u)
@@ -93,21 +92,6 @@ jobs:
- name: ↩️ Checkout
uses: actions/checkout@v7
- name: Copy templates into addon build context
env:
ADDON: ${{ matrix.addon }}
run: |
set -euo pipefail
TEMPLATES_DIR=".templates"
ADDON_DIR="./$ADDON"
# Keep PR builds aligned with the production builder.
for script in ha_automodules.sh ha_autoapps.sh ha_entrypoint.sh bashio-standalone.sh ha_lsio.sh; do
if [ -f "$TEMPLATES_DIR/$script" ]; then
cp "$TEMPLATES_DIR/$script" "$ADDON_DIR/$script"
fi
done
- name: ℹ️ Gather addon info
id: information
uses: frenck/action-addon-information@v1.4
@@ -224,7 +208,6 @@ jobs:
uses: docker/build-push-action@v7
with:
context: ${{ matrix.addon }}
platforms: linux/arm64
push: false
load: true
file: ${{ matrix.addon }}/Dockerfile

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.06.0
uses: home-assistant/builder/actions/build-image@2026.03.2
with:
arch: ${{ matrix.arch }}
cache-gha: "false"
@@ -433,3 +433,4 @@ jobs:
done
git push origin HEAD:master

View File

@@ -56,19 +56,19 @@ If you want to do add the repository manually, please follow the procedure highl
### Number of addons
- In the repository : 136
- Installed : 626324
- In the repository : 135
- Installed : 508490
### Top 3
1. Arpspoof (86665x)
2. Sponsorblockcast (82801x)
3. Jellyfin (71957x)
1. Arpspoof (59486x)
2. Sponsorblockcast (55582x)
3. Flaresolverr (44486x)
### Architectures used
- amd64: 90%
- aarch64: 10%
- amd64: 88%
- aarch64: 12%
### Stars evolution
@@ -77,7 +77,7 @@ If you want to do add the repository manually, please follow the procedure highl
## Add-ons provided by this repository
%%ADDONS_LIST%%
✓ [Arpspoof (86665x)](arpspoof/) : block internet connection for local network devices
✓ [Arpspoof (59486x)](arpspoof/) : block internet connection for local network devices
  ![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Farpspoof%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%2Farpspoof%2Fupdater.json)
@@ -143,17 +143,6 @@ If you want to do add the repository manually, please follow the procedure highl
![amd64][amd64-badge]
![ingress][ingress-badge]
✓ ![image](https://api.iconify.design/mdi/bird.svg) [BirdNET-Pi (zach7036)](birdnet-pi-zach/) : Realtime acoustic bird classification system
  ![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fbirdnet-pi-zach%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%2Fbirdnet-pi-zach%2Fupdater.json)
![aarch64][aarch64-badge]
![amd64][amd64-badge]
![ingress][ingress-badge]
![mqtt][mqtt-badge]
![smb][smb-badge]
![localdisks][localdisks-badge]
✓ ![image](https://api.iconify.design/mdi/bird.svg) [BirdNET-PiPy](birdnet-pipy/) : BirdNET-PiPy bird detection with a modern web dashboard
  ![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fbirdnet-pipy%2Fconfig.yaml)
@@ -258,15 +247,13 @@ If you want to do add the repository manually, please follow the procedure highl
![amd64][amd64-badge]
![ingress][ingress-badge]
✓ ![image](https://api.iconify.design/mdi/robot-happy.svg) [Claude Desktop](claude_desktop/) : Claude Desktop with Headroom, RTK, and TokenSave optimization
✓ ![image](https://api.iconify.design/mdi/robot-happy.svg) [Claude Desktop](claude_desktop/) : Claude Desktop streamed through a browser with LinuxServer Selkies
  ![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)
![aarch64][aarch64-badge]
![amd64][amd64-badge]
![ingress][ingress-badge]
![smb][smb-badge]
![localdisks][localdisks-badge]
✓ ![image](https://api.iconify.design/mdi/movie-search.svg) [Cleanuparr](cleanuparr/) : Automatically removes stuck and unwanted downloads from your *arr and download clients
@@ -315,7 +302,6 @@ If you want to do add the repository manually, please follow the procedure highl
✓ [Elasticsearch server](elasticsearch/) : Free and Open, Distributed, RESTful Search Engine
  ![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]
@@ -522,7 +508,7 @@ If you want to do add the repository manually, please follow the procedure highl
![smb][smb-badge]
![localdisks][localdisks-badge]
✓ ![image](https://api.iconify.design/mdi/billiards-rack.svg) [Jellyfin (71957x) NAS](jellyfin/) : A free Software Media System that puts you in control of managing and streaming your media
✓ ![image](https://api.iconify.design/mdi/billiards-rack.svg) [Jellyfin NAS](jellyfin/) : A free Software Media System that puts you in control of managing and streaming your media
  ![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fjellyfin%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%2Fjellyfin%2Fupdater.json)
@@ -585,7 +571,7 @@ If you want to do add the repository manually, please follow the procedure highl
![aarch64][aarch64-badge]
![amd64][amd64-badge]
✓ ![image](https://api.iconify.design/mdi/movie-search.svg) [Maintainerr](maintainerr/) : Rule-based media cleanup tool for Plex, Jellyfin (71957x) and Emby. Creates collections and optionally deletes unwatched content.
✓ ![image](https://api.iconify.design/mdi/movie-search.svg) [Maintainerr](maintainerr/) : Rule-based media cleanup tool for Plex, Jellyfin and Emby. Creates collections and optionally deletes unwatched content.
  ![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fmaintainerr%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%2Fmaintainerr%2Fupdater.json)
@@ -899,7 +885,7 @@ If you want to do add the repository manually, please follow the procedure highl
![smb][smb-badge]
![localdisks][localdisks-badge]
✓ ![image](https://api.iconify.design/mdi/movie-search.svg) [Seerr](seerr/) : Open-source media request and discovery manager for Jellyfin (71957x), Plex, and Emby
✓ ![image](https://api.iconify.design/mdi/movie-search.svg) [Seerr](seerr/) : Open-source media request and discovery manager for Jellyfin, Plex, and Emby
  ![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fseerr%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%2Fseerr%2Fupdater.json)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -1,7 +1,4 @@
## 1.76.52 (2026-07-11)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 1.76.51 (2026-06-17)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 1.76.49 (2026-06-05)

View File

@@ -1,5 +1,5 @@
name: Aurral
version: "1.76.52"
version: "1.76.51"
slug: aurral
description: >-
Self-hosted music discovery, request management, flows, and playlist

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1,9 +1,9 @@
{
"last_update": "2026-07-11",
"last_update": "2026-06-17",
"repository": "alexbelgium/hassio-addons",
"slug": "aurral",
"source": "github",
"upstream_repo": "lklynet/aurral",
"upstream_version": "1.76.52",
"upstream_version": "1.76.51",
"github_beta": false
}

View File

@@ -1,7 +1,4 @@
## 1.82.1 (2026-07-16)
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
## 1.81.0 (2026-07-04)
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
## 1.80.0 (2026-06-05)

View File

@@ -16,7 +16,7 @@
ARG BUILD_FROM
ARG BUILD_VERSION
ARG BUILD_UPSTREAM="1.82.1"
ARG BUILD_UPSTREAM="1.81.0"
FROM ${BUILD_FROM}
##################

View File

@@ -108,4 +108,4 @@ schema:
slug: autobrr
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/autobrr
version: "1.82.1"
version: "1.81.0"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,8 +1,8 @@
{
"last_update": "2026-07-16",
"last_update": "2026-07-04",
"repository": "alexbelgium/hassio-addons",
"slug": "autborr",
"source": "github",
"upstream_repo": "autobrr/autobrr",
"upstream_version": "1.82.1"
"upstream_version": "1.81.0"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -1,11 +1,3 @@
## source-20260717 (17-07-2026)
- Minor bugs fixed
## source-20260716.02 (16-07-2026)
- Minor bugs fixed
## source-20260716 (16-07-2026)
- Minor bugs fixed
## source-20260714 (14-07-2026)
- Minor bugs fixed
## source-20260709 (09-07-2026)
- Minor bugs fixed
## source-20260708-4 (08-07-2026)

View File

@@ -127,5 +127,5 @@ slug: birdnet-go-dev
udev: true
url: https://github.com/alexbelgium/hassio-addons
usb: true
version: "source-20260717"
version: "source-20260709"
video: true

View File

@@ -77,27 +77,10 @@ for entry in "${prs[@]}"; do
# Fetch the PR head commit by number; works unauthenticated for public repos.
git fetch --no-tags origin "refs/pull/${number}/head"
if ! git merge --no-edit --no-ff -m "Merge PR #${number}: ${title}" "${sha}"; then
mapfile -t conflicted_files < <(git diff --name-only --diff-filter=U)
# package-lock.json is generated content and stacked PRs can carry an
# older copy even when their source changes merge cleanly. Keep the
# lockfile already assembled from upstream and earlier PRs, but only
# when it is the sole conflict. Any source conflict remains fatal.
if [ "${#conflicted_files[@]}" -eq 1 ] \
&& [ "${conflicted_files[0]}" = "frontend/package-lock.json" ]; then
log "Resolving generated frontend/package-lock.json conflict using the accumulated tree"
git checkout --ours -- frontend/package-lock.json
git add frontend/package-lock.json
git commit --no-edit
else
echo "!!! Merge conflict while merging PR #${number} (${title})." >&2
if [ "${#conflicted_files[@]}" -gt 0 ]; then
printf '!!! Conflicting file: %s\n' "${conflicted_files[@]}" >&2
fi
echo "!!! Resolve the conflict in the fork or pause this PR, then rebuild." >&2
git merge --abort || true
exit 1
fi
echo "!!! Merge conflict while merging PR #${number} (${title})." >&2
echo "!!! Resolve the conflict in the fork or pause this PR, then rebuild." >&2
git merge --abort || true
exit 1
fi
done

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,9 +1,3 @@
## 20260716 (2026-07-16)
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
## 20260712 (2026-07-13)
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
## nightly-20260615-4 (09-07-2026)
- Minor bugs fixed
## nightly-20260615-3 (05-07-2026)

View File

@@ -128,4 +128,4 @@ slug: birdnet-go
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-go
usb: true
version: "20260716"
version: "nightly-20260615-4"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -2,10 +2,10 @@
"github_beta": true,
"github_exclude": "-4",
"github_fulltag": true,
"last_update": "2026-07-16",
"last_update": "2026-06-17",
"repository": "alexbelgium/hassio-addons",
"slug": "birdnet-go",
"source": "github",
"upstream_repo": "tphakala/birdnet-go",
"upstream_version": "20260716"
"upstream_version": "nightly-20260615"
}

View File

@@ -1,6 +1,3 @@
## 2026.07.10-1 (10-07-2026)
- Fix standalone (no-Supervisor) mode: strip `sudo` from BirdNET-Pi scripts at build time (the previous hook ran before the repo was cloned, so `sudo` remained and broke service/WebUI actions run by non-sudoers users such as `caddy`/`abc`)
- Ensure `/run/php` exists before starting PHP-FPM so the WebUI starts even when `/run` is a fresh tmpfs
## 2026.07.10 (10-07-2026)
- Minor bugs fixed
## 2026.06.01 (19-06-2026)

View File

@@ -74,6 +74,17 @@ RUN \
curl -f -L -s -S "https://raw.githubusercontent.com/alexbelgium/BirdNET-Pi/main/newinstaller.sh" -o /newinstaller.sh && \
chmod 777 /newinstaller.sh && \
\
# Use installer to modify other scripts
#######################################
# Define file
sed -i "1a /./newinstallermod.sh" /newinstaller.sh && \
echo '#!/bin/bash' >> /newinstallermod.sh && \
# Remove all instances of sudo from all other scripts
echo 'for file in $(grep -srl "sudo" $HOME/BirdNET-Pi/scripts); do sed -i "s|sudo ||" "$file"; done' >> /newinstallermod.sh && \
echo 'for file in $(grep -srl "my_dir" $HOME/BirdNET-Pi/scripts); do sed -i "s|\$my_dir|/config|" "$file"; done' >> /newinstallermod.sh && \
# Set permission
chmod +x /newinstallermod.sh && \
\
# Modify installer
##################
# Use my repository
@@ -99,16 +110,6 @@ RUN \
# Execute installer
/./newinstaller.sh && \
\
# Remove sudo from the installed BirdNET-Pi scripts.
# Inside the container everything already runs with the privileges it needs
# (root during init, user "pi" for the services), so "sudo" is never required.
# Leaving it in breaks any script invoked by a user that is not in sudoers -
# e.g. php-fpm's "caddy" user driving the WebUI System Controls, or the "abc"
# user - which is what prevented BirdNET-Pi from working in standalone
# (no-Supervisor) mode. This must run AFTER the installer has cloned the repo;
# the previous build-time hook ran before the clone and was therefore a no-op.
for file in $(grep -srlF "sudo " "$HOME/BirdNET-Pi/scripts"); do sed -i "s|sudo ||g" "$file"; done && \
\
# Install dateparser and resampy, upgrade numpy
$PYTHON_VIRTUAL_ENV /usr/bin/pip3 install dateparser resampy && \
\

View File

@@ -116,5 +116,5 @@ tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-pi
usb: true
version: 2026.07.10-1
version: 2026.07.10
video: true

View File

@@ -4,14 +4,6 @@
# Correct /config permissions after startup
chown pi:pi /config
# Ensure the PHP-FPM runtime directory exists. It is normally created by
# systemd-tmpfiles (/usr/lib/tmpfiles.d/php*-fpm.conf), which does not run in
# this container - so on a fresh/tmpfs /run the socket cannot be bound and the
# WebUI never starts. Create it here so the web stack works with or without
# Home Assistant Supervisor.
mkdir -p /run/php
chown www-data:www-data /run/php 2> /dev/null || true
# Set timezone without requiring Home Assistant Supervisor or D-Bus.
TZ_VALUE="${TZ:-}"
if [[ -S /var/run/dbus/system_bus_socket ]] && command -v timedatectl > /dev/null 2>&1; then

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -1,10 +1,4 @@
## 0.8.4 (2026-07-13)
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
## 0.8.3 (2026-07-11)
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
## 0.8.2.1 (2026-07-05)
- Re-tag of 0.8.2-1 with no content change. Home Assistant compares add-on versions with semver semantics, where a `-N` suffix counts as a *pre-release* and sorts **below** the base version — so users already on 0.8.2 saw the 0.8.2-1 nginx fix as "Up-to-date" with the Update button disabled. Four-segment `0.8.2.1` sorts above both `0.8.2` and `0.8.2-1` (and below the next upstream `0.8.3`), so the update becomes installable everywhere.

View File

@@ -96,4 +96,4 @@ schema:
ssl: bool?
slug: birdnet-pipy
url: https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-pipy
version: "0.8.4"
version: "0.8.2.1"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,8 +1,8 @@
{
"last_update": "2026-07-13",
"last_update": "2026-07-04",
"repository": "alexbelgium/hassio-addons",
"slug": "birdnet-pipy",
"source": "github",
"upstream_repo": "Suncuss/BirdNET-PiPy",
"upstream_version": "0.8.4"
"upstream_version": "0.8.2"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,10 +1,4 @@
## 1.92.140-ls113 (2026-07-16)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.92.139-ls112 (2026-07-11)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.92.134-ls109 (2026-07-04)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)

View File

@@ -69,5 +69,5 @@ slug: brave
tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "1.92.140-ls113"
version: "1.92.134-ls109"
video: true

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,9 +1,9 @@
{
"github_fulltag": "true",
"last_update": "2026-07-16",
"last_update": "2026-07-04",
"repository": "alexbelgium/hassio-addons",
"slug": "brave",
"source": "github",
"upstream_repo": "linuxserver/docker-brave",
"upstream_version": "1.92.140-ls113"
"upstream_version": "1.92.134-ls109"
}

View File

@@ -1,10 +1,4 @@
## version-ae9a7fca (2026-07-16)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
## version-30a7c401 (2026-07-11)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
## version-7148c2a3 (2026-07-04)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)

View File

@@ -71,5 +71,5 @@ slug: chromium
tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "version-ae9a7fca"
version: "version-7148c2a3"
video: true

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -1,9 +1,9 @@
{
"github_fulltag": "true",
"last_update": "2026-07-16",
"last_update": "2026-07-04",
"repository": "alexbelgium/hassio-addons",
"slug": "chromium",
"source": "github",
"upstream_repo": "linuxserver/docker-chromium",
"upstream_version": "version-ae9a7fca"
"upstream_version": "version-7148c2a3"
}

View File

@@ -1,7 +1,4 @@
## 2.55.0 (2026-07-16)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.54.2 (2026-07-04)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)

View File

@@ -86,5 +86,5 @@ schema:
slug: browserless_chrome
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/browserless_chrome
version: "2.55.0"
version: "2.54.2"
webui: "[PROTO:ssl]://[HOST]:[PORT:3000]/docs"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -1,9 +1,9 @@
{
"github_tagfilter": "v",
"last_update": "2026-07-16",
"last_update": "2026-07-04",
"repository": "alexbelgium/hassio-addons",
"slug": "browserless_chrome",
"source": "github",
"upstream_repo": "browserless/chrome",
"upstream_version": "2.55.0"
"upstream_version": "2.54.2"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,6 +1,3 @@
## 0.55.8 (2026-07-16)
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
## 0.55.7 (2026-05-30)
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)

View File

@@ -34,4 +34,4 @@ schema:
slug: changedetection.io
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/changedetection.io
version: "0.55.8"
version: "0.55.7"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,9 +1,9 @@
{
"github_fulltag": "false",
"last_update": "2026-07-16",
"last_update": "2026-05-30",
"repository": "alexbelgium/hassio-addons",
"slug": "changedetection.io",
"source": "github",
"upstream_repo": "linuxserver/docker-changedetection.io",
"upstream_version": "0.55.8"
"upstream_version": "0.55.7"
}

View File

@@ -1,107 +1,3 @@
## 1.32 (17-07-2026)
- Bump `tokensave` from 7.2.0 to 7.4.0 (`rtk` was already pinned to its current latest GitHub release, `v0.43.0`; `headroom-ai` is intentionally installed unpinned from PyPI, so it already tracks latest at every build and had nothing to bump). Reviewed the intervening 7.3.0/7.4.0 release notes against every tokensave surface this add-on drives (`install --agent claude --git-hook yes`, `uninstall --agent claude`, `sync`, `init`, `doctor --agent claude`, `gain --all --range 30d`, and the `mcp__tokensave__*` tool set granted in `settings.json`): no flag, output shape, or MCP tool name used here changed. Directly relevant fixes carried along: `tokensave sync` auto-migrates a v12 database (missing the trait-dispatch caller cache) to v13 as a normal part of syncing, which the add-on's corruption-quarantine logic won't mistake for corruption since the schema migration doesn't produce a "malformed"/"not a database" error; and `install`/`uninstall`'s JSON writer now resolves a symlinked `~/.claude/settings.json` before its atomic rename instead of replacing the symlink with a plain file, so a dotfiles-managed settings file survives untouched.
- Fix startup TokenSave repository preparation silently doing nothing: both `81-tokensave_repositories.sh` and the indexing loop in `82-claude_tools.sh` read `tokensave_project_paths` through `done < <(bashio::config ...)`, but `bashio::config` begins with a `read -d ''` heredoc that always returns non-zero, and a process substitution inherits the `errexit` enabled by the bashio wrapper itself — so the subshell died before printing and every boot iterated over an empty list (no `safe.directory` persistence, no startup `sync`/`init`; command substitutions were unaffected because subshells drop `errexit` when `inherit_errexit` is off, which is why every other option lookup worked). Repositories only got indexed when tokensave's own git hooks or a manual `tokensave init` happened to run. The list is now captured with a command substitution first and the loop reads from the captured variable (here-string); same fix applied to `claude-tools-doctor.sh`. The indexing loop also moved ahead of the MCP-registration merge, and the merge now re-tightens the 0600 mode on the token-bearing configs even on no-change boots — a first-time `tokensave init` rewrites `~/.claude.json` itself at default permissions, which previously could leave the stored Home Assistant token world-readable until the next registration change.
- Simplification pass over the startup logic: every remaining line now serves a live purpose, with no change to what gets configured — Headroom (proxy routing, MCP registration, CLAUDE.md guidance, PostToolUse auto-compression, dashboard exposure), RTK (global files + PreToolUse hook), and TokenSave (full agent integration + per-repo indexing) are still applied automatically to every new session type (terminal, Desktop cowork/dispatch, cron).
- `82-claude_tools.sh`: the three hand-rolled `~/.claude/settings.json` hook mutators (rtk add, rtk remove, headroom PostToolUse) are replaced by one shared `manage_settings_hook` helper using the proven strip-then-re-append pass (same dedup/matcher-migration semantics; additionally no longer creates an empty `settings.json` when asked to remove a hook from a machine that never had one). The two copy-pasted CLAUDE.md guidance managers (headroom, ha-api-helper) collapse into one `manage_claude_md_block` helper producing byte-identical blocks, so existing installs are recognized without a rewrite.
- `81-tokensave_repositories.sh` is merged into the TokenSave loop of `82-claude_tools.sh`: the same path list was parsed twice with identical trimming/validation only so `safe.directory` could be persisted before repository detection ran as root. Detection now runs directly as the runtime user with a one-shot `safe.directory` override (the persisted entry is still written for tokensave's git hooks and Claude sessions), removing the duplicate loop and the root-reads-abc-gitconfig coupling. The battle-tested defensive sync/init block (flock, retries, corruption quarantine, init sentinel) is unchanged.
- The `/tmp/claude-desktop-command` indirection is gone: `82-claude_tools.sh` wrote the default launch command to a file that only `defaults/autostart` read, with the identical default hardcoded as its fallback — nothing else ever wrote it. `autostart` now launches Claude Desktop directly (keyring bootstrap unchanged).
- `82-claude_tools.sh` no longer ends with its own recursive chown of `~/.claude`, `~/.claude.json`, and `~/.config/Claude`: `84-claude_runtime_ownership.sh` already reconciles exactly those paths after all Claude configuration scripts have run.
- `83-claude_permissions.sh` drops the hidden `.addon-permission-mode.json` state file in favor of the same managed-value semantics used for `ANTHROPIC_BASE_URL`: `auto`/`bypass` set `permissions.defaultMode`, `strict` removes it only while it still holds an add-on-managed value (`auto`/`bypassPermissions`), and a hand-set custom value is never deleted. The old restore-from-state behavior could resurrect a stale value recorded on the first managed boot; the stale state file is cleaned up on upgrade.
- `80-configuration.sh` sheds branches that were unreachable in this image: `apk`/`pacman` installers (the base is Debian), the `pip3` fallback (`uv` is always baked in), and the no-op timezone error path (invalid `TZ` values are now actually detected against `/usr/share/zoneinfo` before the symlink is written).
- Remove the dead `auto_update` option from `config.yaml` and the README: its schema entry was removed back in 1.22 and `81-claude_update.sh` has updated Claude Desktop unconditionally (best-effort, offline-safe) ever since; the README now states that behavior instead of documenting a switch that did nothing.
## 1.31 (16-07-2026)
- Pin the LinuxServer selkies base image to a fixed version (`…-debianbookworm-45960cc3-ls113`) instead of the rolling `…-debianbookworm` tag. The rolling tag is rebuilt continuously (and itself installs selkies "latest" at base-build time), so the desktop/stream runtime could change under the add-on with no change to its own files — builds are now reproducible and the base only moves when this value is bumped deliberately. The pinned tags resolve to exactly the image the rolling tag currently points at (amd64 `sha256:6a4d5154…`, aarch64 `sha256:90914dfd…`).
- Fix Claude Desktop never appearing — the Selkies web client stayed on "waiting for stream" forever with `libEGL warning: failed to open /dev/dri/card0: Permission denied` in the log. The LinuxServer base image grants the desktop user (`abc`) access to the `/dev/dri` render nodes in its `init-video` s6 oneshot, but that oneshot is not a dependency of `svc-xorg`/`svc-selkies`/`svc-de`, so on Home Assistant those long-running services regularly start (via `s6-setuidgid abc`) *before* `abc` has been added to the render group. Xorg/Selkies/pixelflux then open the render device without permission, the video pipeline produces no frames, and the stream never starts. Prepare the exposed DRI nodes in a new `21-gpu_permissions.sh` cont-init script instead: `cont-init.d` runs to completion before any s6-rc service starts, so `abc` is added to each node's owning group (and the node is made world read/write as a timing-independent fallback) in time for the graphical services to use the GPU. Best-effort and a no-op on hosts that expose no GPU.
## 1.30 (16-07-2026)
- Compress large tool outputs automatically in every Claude Code session with a managed `PostToolUse` hook (new `headroom_auto_compress` option, enabled by default). Desktop-spawned sessions (cowork/dispatch) pin `ANTHROPIC_BASE_URL` to the production endpoint (headroom #869), so the transparent proxy never sees their traffic and compression there depended entirely on the model remembering to call the `headroom` MCP tools per the CLAUDE.md guidance — in practice most large outputs went uncompressed. The new `/usr/local/bin/headroom-posttooluse-compress.py` hook fires on `Bash`/`Grep`/`Glob`/`WebFetch` results over ~4000 characters, compresses them with Headroom's rule-based pipeline (SmartCrusher and friends; the Kompress ML path is disabled because its background model load can never complete inside a short-lived hook process), and swaps the result in via `hookSpecificOutput.updatedToolOutput` with a retrieval marker appended. Originals are stored in the shared CCR SQLite store (`~/.headroom/ccr_store.db` — the same one the headroom MCP server reads), so `mcp__headroom__headroom_retrieve` always recovers the full output; savings are recorded to the durable ledger (client `posttooluse-hook`) and show up in the existing gains report. The hook fails open (any error leaves the tool output untouched), never touches `stderr` fields so error text reaches the model verbatim, skips anything below a 50-token savings floor, and is registered idempotently in `~/.claude/settings.json` only after a `--self-test` confirms the interpreter can import headroom; disabling the option (or Headroom) removes the managed entry without touching user-defined hooks. Measured on a representative Home Assistant `states` dump: 10781 -> 2964 tokens (73% saved) at ~1.7 s hook overhead, with sub-100 ms pass-through for small outputs.
## 1.29 (16-07-2026)
- Point the Headroom MCP server at the persistent Kompress model cache. 1.27 set `HF_HOME` on the `svc-headroom` proxy longrun only, but the MCP server is a separate process spawned by Claude Desktop / Claude Code from the registered `mcpServers` entry, so it never inherited that export and kept resolving the HuggingFace cache to `~/.cache` — symlinked to tmpfs here and wiped on every restart. Its Kompress ML path therefore never found the model, re-downloaded ~270 MB into tmpfs on each boot, and lost it again on the next one; `headroom_compress` fell back to `router:noop` (unchanged output) on prose and other unstructured content. The managed `headroom` entry in both `claude_desktop_config.json` and `~/.claude.json` now carries `env.HF_HOME` pointing at the same `~/.headroom/hf` cache the proxy warms. Rule-based compression (SmartCrusher, structured tool output) was unaffected and worked throughout.
- Fix `~/.gitconfig` being written as `root` and left unreadable by the `abc` runtime user, which broke git for the user that actually runs it: every commit failed with `Author identity unknown` and the `gh` credential helper was invisible to authenticated pushes. `git config --global` ran as root during init and rewrites the file on every start, so `20-folders.sh`'s earlier recursive chown never stuck to it (`.config/gh` survived abc-owned only because the "already authenticated" branch skips rewriting it). The git/gh setup now runs as `abc` via `s6-setuidgid`, matching `81-tokensave_repositories.sh`, and reclaims any root-owned copies left by an earlier version before writing.
- Fix `~/.bashrc` accumulating stale `HOME`/`FM_HOME` exports when `data_location` changes. The idempotency guard only tested for the *current* `$LOCATION`, so changing the option and later changing it back appended a second block while leaving the first, and the last one written won for every interactive shell — leaving `$HOME` pointing at a directory the add-on no longer manages. Any tool that resolves config through `$HOME` then read the wrong path (`headroom doctor` reported `claude: not routed (no ~/.claude/settings.json)` against a correctly routed install, and bare `headroom` invocations created a stray `.headroom` tree under the old location). The block is now marker-delimited and rewritten from scratch on every boot, so it is idempotent across any number of `data_location` changes.
## 1.27 (15-07-2026)
- Route Claude Desktop cowork/local-agent-mode sessions through the Headroom proxy. Desktop spawns its bundled Claude Code binary at an absolute path (bypassing the add-on's PATH wrapper) with `ANTHROPIC_BASE_URL` pinned to the production endpoint, so those sessions never produced proxy savings. The add-on now manages `env.ANTHROPIC_BASE_URL` in `~/.claude/settings.json` — settings `env` entries replace inherited environment values at CLI startup — gated on `headroom_wrap_claude_code` and never overwriting a user-customized endpoint.
- Fix Headroom's Kompress compression engine never activating, which made even proxied traffic record zero token savings (e.g. 175 requests, 0 saved). The proxy's startup preload is deliberately cache-only, but the HuggingFace model cache defaulted to `~/.cache` — tmpfs in this add-on, wiped every restart — so the ONNX model (plus the separately fetched `answerdotai/ModernBERT-base` tokenizer) was never cached and the engine idled in "deferred" mode forever, misleadingly logged as `Kompress: not installed`. `svc-headroom` now points `HF_HOME` at persistent storage (`~/.headroom/hf`, ~270 MB); the proxy's own request path already downloads a missing model in the background on first use and passes requests through uncompressed until it lands, so no blocking startup pre-warm is needed — the port binds immediately either way, and Kompress activates within the first couple of requests on the first boot, then loads instantly on every boot after. The already-installed `proxy` extra's ONNX runtime is sufficient — the multi-gigabyte PyTorch `ml` extra is deliberately not installed.
## 1.26 (15-07-2026)
- Fix startup permission failures that prevented Claude Desktop from starting: storage was chowned to a hardcoded `1000:1000`, but the shared `abc` desktop user was never mapped to that UID. During init `abc` was still the image default (`911`), so TokenSave (`.claude.json.new`), RTK (`RTK.md`), nginx, PulseAudio, the Mesa shader cache, and Claude Desktop itself all hit `Permission denied`; the base image's `init-adduser` then remapped `abc` to root mid-startup (PUID/PGID were read from add-on options where they did not exist, falling back to `0`), which also made Claude Code reject `permission_mode: bypass`.
- Add `PUID`/`PGID` add-on options (default `1000:1000`) and remap `abc` to that identity at the very start of folder setup, before any ownership is applied and before any service resolves the user. The base image's `init-adduser` is pinned to the same effective identity so it can no longer remap `abc` mid-startup.
- In `permission_mode: bypass`, a configured `PUID: 0` automatically falls back to UID `1000` (Claude Code refuses bypass permissions as root), retaining the configured group.
- Fix `bashio::config.array: command not found` in the TokenSave repository setup, tools configuration, and `claude-tools-doctor.sh`: the function only exists in the repo's standalone bashio, not in the real bashio shipped in the image. Use `bashio::config`, which prints list entries one per line.
- Return managed Claude configuration files to the effective `abc` identity instead of the raw configured `PUID`/`PGID` (which previously fell back to `0` and left the files root-owned).
- Pre-create `/tmp/.X11-unix` with the standard sticky mode so Xorg, which runs as the non-root `abc` user on a tmpfs `/tmp`, no longer fails to create its socket directory (`_XSERVTransmkdir: euid != 0`).
## 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`, using the integration's stateless Streamable HTTP endpoint (`/api/mcp`).
- Write the Claude configuration files with `0600` permissions, since they hold the Home Assistant access token in clear text.
- Restrict the build-time `chmod +x` pass to the directories the add-on actually ships scripts in instead of traversing the whole image.
- Register add-on-managed MCP servers in Claude Code's `~/.claude.json` as well as Claude Desktop's config, without clobbering user-customized entries.
- Install `uv` and use it for the `additional_pip` option for much faster package installs.
## 1.16 (14-07-2026)
- Minor bugs fixed
## 1.15 (13-07-2026)
- 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)
- Build pinned RTK 0.43.0 source on Debian Bookworm for both architectures instead of installing the upstream arm64 release binary, which requires GLIBC 2.39 and cannot run in the add-on image.
- Execute `rtk --version` inside the final image during the Docker build so future ABI incompatibilities fail CI instead of surfacing at runtime.
- Validate the final Bookworm-built RTK binary in a native aarch64 image build.
- Correct the repository PR checks so changed changelog paths are exported and aarch64 images are built explicitly for `linux/arm64`.
## debiantrixie-version-c55d3809 (2026-07-11)
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
## 1.13 (10-07-2026)
- Add the official Claude Code stable package, `tmux`, `ripgrep`, and a pinned upstream `ttyd` binary for both supported architectures.

View File

@@ -9,32 +9,7 @@
ARG BUILD_FROM
ARG BUILD_VERSION
ARG RTK_VERSION="v0.43.0"
ARG RTK_COMMIT="5a7880d404db8364d602f2ecdc41dd790f64013f"
ARG TOKENSAVE_VERSION="7.4.0"
# The upstream aarch64 release is cross-built on ubuntu-latest and requires
# GLIBC 2.39. Build the pinned source on Bookworm instead so it is compatible
# with the add-on runtime on both supported architectures.
FROM rust:1.91-bookworm AS rtk-builder
ARG RTK_VERSION
ARG RTK_COMMIT
RUN git clone --depth 1 --branch "${RTK_VERSION}" https://github.com/rtk-ai/rtk.git /src/rtk && \
test "$(git -C /src/rtk rev-parse HEAD)" = "${RTK_COMMIT}" && \
cd /src/rtk && \
cargo build --release --locked && \
install -D -m 0755 target/release/rtk /out/rtk && \
/out/rtk --version
# tokensave ships no Bookworm-compatible prebuilt binary either; build the pinned
# crates.io release from source so GLIBC matches the add-on runtime.
FROM rust:1.91-bookworm AS tokensave-builder
ARG TOKENSAVE_VERSION
RUN cargo install tokensave --version "${TOKENSAVE_VERSION}" --locked --root /out && \
/out/bin/tokensave --version
FROM ${BUILD_FROM}
ARG BUILD_ARCH
##################
# 2 Modify Image #
@@ -70,21 +45,17 @@ RUN curl -fsSL --retry 3 --retry-delay 2 \
# 3 Install apps #
##################
# Add rootfs. Only the directories this add-on ships scripts in are traversed, so the chmod
# cannot alter executables elsewhere in the image.
# Add rootfs
COPY rootfs/ /
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
RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \; && \
chmod +x /usr/local/bin/claude-direct /usr/local/bin/claude-headroom /usr/local/bin/claude-terminal-shell
# 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, Python tooling, and lightweight local validators.
# gnome-keyring provides the Secret Service backend Electron safeStorage needs to persist
# sign-in and dispatch grants.
# Install Claude Desktop, Claude Code, browser-terminal tooling, and Python tooling
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 && \
@@ -100,51 +71,33 @@ RUN install -d -m 0755 /etc/apt/keyrings && \
git \
gh \
ripgrep \
jq \
shellcheck \
yamllint && \
test -x /usr/bin/claude && \
tmux && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install the current upstream hadolint and actionlint releases for both supported
ARG HADOLINT_VERSION=v2.14.0
ARG ACTIONLINT_VERSION=v1.7.12
# ttyd is not packaged in Debian bookworm. Install the pinned upstream static binary.
ARG TTYD_VERSION="1.7.7"
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 ;; \
case "$(dpkg --print-architecture)" in \
amd64) ttyd_arch="x86_64" ;; \
arm64) ttyd_arch="aarch64" ;; \
*) echo "Unsupported architecture for ttyd: $(dpkg --print-architecture)" >&2; exit 1 ;; \
esac; \
curl -fsSL --retry 3 --retry-delay 2 \
-o /usr/local/bin/hadolint \
"https://github.com/hadolint/hadolint/releases/download/${HADOLINT_VERSION}/hadolint-linux-${hadolint_arch}"; \
chmod 0755 /usr/local/bin/hadolint; \
curl -fsSL --retry 3 --retry-delay 2 \
-o /tmp/actionlint.tar.gz \
"https://github.com/rhysd/actionlint/releases/download/${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION#v}_linux_${actionlint_arch}.tar.gz"; \
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
-o /usr/local/bin/ttyd \
"https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.${ttyd_arch}"; \
chmod 0755 /usr/local/bin/ttyd; \
/usr/local/bin/ttyd --version
# Copy the pinned Bookworm-built RTK 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
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->HTTP bridge for the Home Assistant MCP server) and uv (fast
# installer used for the additional_pip option). The `proxy` extra already ships the ONNX
# runtime + transformers needed by the Kompress compressor — the `ml` extra (full PyTorch,
# ~5 GB with CUDA wheels) is deliberately NOT installed; svc-headroom pre-warms the ONNX
# model into the persistent HF cache instead.
# Install only the Headroom proxy, code-compression, and MCP features used by this add-on.
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 websockets && \
pip3 install --break-system-packages "headroom-ai[proxy,code,mcp]" && \
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh -o /tmp/rtk-install.sh && \
HOME=/root sh /tmp/rtk-install.sh && \
rm /tmp/rtk-install.sh && \
if [ -x /root/.local/bin/rtk ] && [ ! -x /usr/local/bin/rtk ]; then mv /root/.local/bin/rtk /usr/local/bin/rtk; fi && \
if [ -x /usr/local/bin/rtk ]; then chmod +x /usr/local/bin/rtk; else echo "rtk binary was not installed on PATH"; exit 1; fi && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /root/.cache
@@ -192,6 +145,7 @@ CMD [ "/ha_entrypoint.sh" ]
# 5 Labels #
############
ARG BUILD_ARCH
ARG BUILD_DATE
ARG BUILD_DESCRIPTION
ARG BUILD_NAME

View File

@@ -4,9 +4,8 @@
![Supports amd64 Architecture][amd64-shield]
![Project Maintenance][maintenance-shield]
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.
Run Claude Desktop and an optional persistent Claude Code web terminal in one
LinuxServer.io Selkies add-on.
## Installation
@@ -21,252 +20,151 @@ currently does not include Computer Use or dictation.
## Architecture
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, MCP servers, permissions, and
PATH tools.
Claude Desktop and Claude Code run as separate clients inside the same add-on.
They share the configured persistent home directory, Git credentials,
repositories, Claude Code configuration, Headroom storage, and RTK
configuration, but they do not share or hand off a conversation.
- **Claude Desktop** uses Headroom through its MCP tools.
- **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.
- The shared `abc` desktop account runs under the configured `PUID`/`PGID`
(default `1000:1000`). When `permission_mode: bypass` is selected while
`PUID` is `0`, the add-on automatically falls back to UID `1000` before
Selkies and Claude Desktop start, because 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.
- **Claude Code** uses Headroom's supported `headroom wrap claude` integration.
- **RTK** filters Claude Code Bash output through its `PreToolUse` hook.
- **tmux** keeps the terminal session running when the browser disconnects.
## Features
- Claude Desktop in single-app Selkies mode with Home Assistant ingress.
- Official Claude Code stable package powering Desktop cowork/dispatch
sessions.
- Claude Desktop in single-app Selkies mode.
- Home Assistant ingress support for Claude Desktop.
- Official Claude Code stable package installed in the same image.
- Optional authenticated `ttyd` web terminal on port `7681`.
- Persistent `tmux` session shared by reconnecting terminal clients.
- 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.
- Best-effort Claude Desktop update from Anthropic's apt repository at every
startup (skipped silently when offline).
- Optional extra apt and pip package installation (pip installs use `uv`).
- Baked-in `git`, GitHub CLI (`gh`), `ripgrep`, `jq`, `shellcheck`, `yamllint`,
`hadolint`, and `actionlint`.
- Optional runtime Claude Desktop updates from Anthropic's apt repository.
- Optional extra apt and pip package installation.
- Baked-in `git`, GitHub CLI (`gh`), `ripgrep`, and terminal tooling.
- Custom script support through the repository standard `claude_desktop.sh`.
- 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.
- 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.
- Optional bundled Claude Code optimization tools: Headroom, RTK, and Caveman.
- Headroom dashboard exposed on mapped port `8787` when enabled.
- Low-power defaults for GPU mapping, Selkies frame rate, and volatile caches.
## Claude Code terminal setup
The terminal service is enabled in the add-on configuration but remains
unavailable until authentication is configured. Port `7681` is not mapped by
default.
1. Set a unique `terminal_password`. The existing `PASSWORD` option is accepted
only as a compatibility fallback.
2. Optionally set `terminal_username` and `terminal_workspace`.
3. Map container port `7681` to a host port in the add-on **Network** section.
4. Restart the add-on.
5. Reach `http://<home-assistant-host>:7681` only through an encrypted VPN or an
HTTPS reverse proxy, then sign in with the configured terminal credentials.
The terminal opens in a persistent tmux session. Closing the browser detaches
from tmux rather than terminating commands that are already running.
Start the optimized Claude Code path with:
```shell
claude-headroom
```
This reuses the supervised Headroom proxy on `127.0.0.1:8787` and launches
Claude Code with the required routing. Headroom is told not to install RTK
because the add-on already maintains the RTK hook in
`~/.claude/settings.json`.
To bypass Headroom for troubleshooting, run:
```shell
claude-direct
```
Running `claude` directly is equivalent to the direct path. The first Claude
Code launch may require its own account authentication; Desktop and Claude Code
store separate client credentials even though both use the configured
persistent home directory.
### Multiple concurrent clients
Every browser connection attaches to the same tmux session. Concurrent clients
therefore see the same terminal, keystrokes, and resize events. This is useful
for reconnecting to one long-running session, but it is not an isolated
multi-user terminal.
### Terminal user and permissions
The service drops privileges to the LinuxServer `abc` account before starting
ttyd. The effective numeric UID and GID follow the configured `PUID` and `PGID`.
Using `PUID: 0` can provide root-equivalent access inside the add-on; use a
non-zero UID/GID where your storage permissions allow it.
The configured workspace must resolve to the persistent home directory or a
subdirectory of `/share`, `/media`, `/mnt`, `/data`, or `/config`. Existing
directories are never re-owned by the terminal service and must already be
readable, writable, and searchable by `abc`.
### Terminal security
The direct ttyd endpoint uses HTTP Basic Authentication without TLS.
Credentials and terminal traffic are unencrypted on the network. ttyd also
receives its Basic Authentication credential as a process argument, so it is
visible to processes with sufficient access inside the container.
Do not expose port `7681` directly to the public internet. Use a VPN such as
WireGuard or Tailscale, or place the endpoint behind an HTTPS reverse proxy.
Use a unique `terminal_password` rather than reusing the Selkies `PASSWORD`.
## Options
| Option | Default | Description |
| ------ | ------- | ----------- |
| `PUID` / `PGID` | `1000` / `1000` | Numeric user and group of the shared `abc` desktop account that owns the data location and runs Claude Desktop. In bypass mode, a root `PUID` is automatically replaced at runtime by UID `1000` while the configured group is retained. |
| `PUID` / `PGID` | `0` / `0` | Numeric user and group applied by the LinuxServer initialization. |
| `TZ` | | Optional timezone, for example `Europe/Brussels`. |
| `KEYBOARD` | | Optional Selkies keyboard layout. |
| `PASSWORD` | | Optional password for direct Selkies ports. |
| `PASSWORD` | | Optional password for direct Selkies ports and compatibility fallback for terminal authentication. |
| `DRINODE` | | Optional GPU device override for Selkies. |
| `DNS_server` | `8.8.8.8` | DNS server used by the standard DNS module. |
| `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. |
| `headroom_auto_compress` | `true` | Auto-compress large tool outputs in every Claude Code session via a managed `PostToolUse` hook. |
| `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. |
| `auto_update` | `true` | Upgrade `claude-desktop` from Anthropic's apt repository at startup. |
| `enable_terminal` | `true` | Enable the supervised Claude Code web-terminal service. |
| `terminal_username` | `claude` | Username used by ttyd Basic Authentication. |
| `terminal_password` | | Dedicated terminal password. The service idles when this and `PASSWORD` are empty. |
| `terminal_workspace` | | Initial directory; defaults to `<data_location>/workspace`. |
| `install_headroom` | `true` | Enable Headroom MCP for Desktop and the supervised local proxy reused by `claude-headroom`. |
| `install_rtk` | `true` | Configure RTK's Claude Code `PreToolUse` hook. |
| `install_caveman` | `true` | Install the Caveman Claude Code plugin in the persistent Claude home. |
| `install_github_cli` | `true` | Enable setup checks for the baked-in `git` and `gh` commands. |
| `github_token` | | Optional GitHub token used to authenticate `gh` and Git operations. |
| `github_username` | | Optional global Git author name. |
| `github_email` | | Optional global Git author email. |
| `enable_ha_mcp` | `false` | Register Home Assistant's MCP server in Claude (requires `ha_mcp_token`). |
| `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. |
| `ha_smart_context` | `true` | Enable Home Assistant smart context support for Claude tooling. |
| `enable_ha_mcp` | `true` | Enable Home Assistant MCP support for Claude tooling. |
| `dangerously_skip_permissions` | `false` | Reserved compatibility option; it is not applied by the terminal launcher. |
| `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. |
| `additional_pip` | | Comma-separated pip packages installed with `--break-system-packages`. |
| `data_location` | `/data/data` | Persistent home directory for both Claude clients 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` runs the shared `abc`
runtime account as UID `1000` instead, 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`
with the explicit local proxy URL in Claude Desktop and Claude Code, then starts
a supervised Headroom backend on `127.0.0.1:8787`.
When `install_headroom` is enabled, the add-on registers `headroom mcp serve` in
Claude Desktop and starts a supervised local Headroom backend. Desktop can use
`headroom_compress`, `headroom_retrieve`, and `headroom_stats` through MCP.
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.
Claude Desktop overrides `ANTHROPIC_BASE_URL`, so it is deliberately launched
without proxy injection. The web terminal instead provides `claude-headroom`,
which reuses the supervised proxy through Headroom's `--no-proxy` mode. RTK
setup remains owned by the add-on through Headroom's `--no-rtk` mode.
With `headroom_auto_compress` enabled (the default), a managed Claude Code
`PostToolUse` hook additionally compresses large `Bash`/`Grep`/`Glob`/`WebFetch`
outputs (over ~4000 characters) in **every** session type — terminal, Desktop
cowork, dispatch, and cron — without the model having to remember to call the
MCP tools. The original output is kept in Headroom's local store for one hour
and can always be recovered with `mcp__headroom__headroom_retrieve` using the
hash printed in the compression marker. Error text (`stderr`) is never
compressed, and plain prose passes through unchanged; the savings come from
structured output such as JSON dumps, search results, and logs.
The Headroom dashboard remains available at:
The dashboard is disabled externally by default. To expose it:
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
```text
http://<home-assistant-host>:8787/dashboard
```
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
To let Claude query and control Home Assistant:
1. In Home Assistant, add the **Model Context Protocol Server** integration
(Settings → Devices & services → Add integration).
2. Create a long-lived access token (your profile → Security).
3. Set `enable_ha_mcp: true` and paste the token into `ha_mcp_token` in the
add-on configuration, then restart the add-on.
The add-on bridges Claude to the integration's stateless Streamable HTTP
endpoint (`/api/mcp`) with `mcp-proxy`. Override `ha_mcp_url` only if your Home
Assistant instance is not reachable as `homeassistant:8123` from add-ons.
## 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.
when the `8787/tcp` port is mapped. Treat this endpoint as sensitive and do not
expose it directly to the public internet.
## Custom scripts
@@ -280,14 +178,10 @@ the image.
Persistent state is stored in the configured `data_location` (default
`/data/data`):
- Claude Desktop sign-in: `~/.config/Claude` (token encrypted via
gnome-keyring; keyring DB in `~/.local/share/keyrings`)
- 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
- Claude Desktop sign-in: `~/.config/Claude`
- Claude Code settings, hooks, sessions, and plugins: `~/.claude`
- Default terminal workspace: `~/workspace`
- Headroom and RTK user state: their standard paths below the shared home
Volatile cache data is redirected to `/tmp/cache` through `$XDG_CACHE_HOME` and
`$HOME/.cache`.

View File

@@ -4,10 +4,7 @@ Two related sign-in problems when Claude Desktop runs inside the LinuxServer Sel
streamed desktop.
**Status:**
- **Shipped:** Problem B (keyring persistence) — the `autostart` bootstrap landed in v1.4, but
the `gnome-keyring` package itself was missing from the image until v1.17 (the bootstrap
silently no-oped and Electron logged "safeStorage encryption is not available"). Fixed in
v1.17: the Dockerfile now installs `gnome-keyring`.
- **Shipped:** Problem B (keyring persistence) is implemented in v1.4 (Dockerfile + `rootfs/defaults/autostart`).
- **Planned only:** Problem A (in-desktop browser for OAuth) is intentionally not implemented.
The image ships no browser; complete the login with the user-side workaround below.
@@ -53,9 +50,8 @@ magic link into the in-session Chromium (not a phone).
### User-side workaround (no rebuild)
- Add-on Configuration → `additional_apps: chromium`, restart (installed by
`rootfs/etc/cont-init.d/80-configuration.sh`).
- Add the two `xdg-settings`/`xdg-mime` commands to the custom script
`/addon_configs/db21ed7f_claude-desktop/claude_desktop.sh` (the image ships no standalone
terminal).
- Run the two `xdg-settings`/`xdg-mime` commands once in an in-session terminal, or add them
to the custom script `/addon_configs/db21ed7f_claude-desktop/claude-desktop.sh`.
---
@@ -98,14 +94,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=/data/data`
3. Persistence: the keyring DB lives in `$HOME/.local/share/keyrings/` and `HOME=/config/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

@@ -1,6 +1,6 @@
{
"build_from": {
"aarch64": "ghcr.io/linuxserver/baseimage-selkies:arm64v8-debianbookworm-45960cc3-ls113",
"amd64": "ghcr.io/linuxserver/baseimage-selkies:amd64-debianbookworm-45960cc3-ls113"
"aarch64": "ghcr.io/linuxserver/baseimage-selkies:arm64v8-debianbookworm",
"amd64": "ghcr.io/linuxserver/baseimage-selkies:amd64-debianbookworm"
}
}

View File

@@ -2,7 +2,7 @@ arch:
- aarch64
- amd64
audio: true
description: "Claude Desktop with Headroom, RTK, and TokenSave optimization"
description: Claude Desktop with a persistent Claude Code web terminal in one add-on
devices:
- /dev/dri
- /dev/dri/card0
@@ -13,8 +13,8 @@ environment:
AUTO_GPU: "1"
FM_HOME: /data/data
HOME: /data/data
PGID: "1000"
PUID: "1000"
PGID: "0"
PUID: "0"
SELKIES_FRAMERATE: "30"
START_DOCKER: "false"
TITLE: Claude Desktop
@@ -34,37 +34,36 @@ name: Claude Desktop
options:
env_vars: []
DNS_server: 8.8.8.8
PGID: 1000
PUID: 1000
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/api/mcp
ha_mcp_token: ""
enable_ha_api_helper: true
ha_smart_context: true
enable_ha_mcp: true
dangerously_skip_permissions: false
enable_terminal: true
terminal_username: claude
terminal_password: ""
terminal_workspace: ""
github_token: ""
github_username: ""
enable_tools_health_report: true
expose_headroom_dashboard: false
headroom_auto_compress: true
headroom_wrap_claude_code: true
install_caveman: false
install_caveman: true
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: null
7681/tcp: null
8787/tcp: 8787
ports_description:
3001/tcp: Claude Desktop web interface
8787/tcp: Optional Headroom dashboard and proxy
7681/tcp: HTTP Basic-auth Claude Code terminal (no TLS)
8787/tcp: Headroom dashboard and proxy
privileged:
- SYS_ADMIN
- DAC_READ_SEARCH
@@ -82,33 +81,24 @@ schema:
TZ: match([A-Z][a-z]*./[A-Z][a-z]*.)?
additional_apps: str?
additional_pip: str?
cifsdomain: str?
cifspassword: str?
cifsusername: str?
localdisks: str?
networkdisks: str?
auto_update: bool?
github_email: str?
ha_smart_context: bool?
enable_ha_mcp: bool?
ha_mcp_url: str?
ha_mcp_token: password?
enable_ha_api_helper: bool?
dangerously_skip_permissions: bool?
enable_terminal: bool?
terminal_username: match(^[A-Za-z0-9_.-]+$)?
terminal_password: password?
terminal_workspace: str?
github_token: password?
github_username: str?
enable_tools_health_report: bool
expose_headroom_dashboard: bool
headroom_auto_compress: 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.32"
version: "1.13"
video: true

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -12,7 +12,24 @@ export GNOME_KEYRING_CONTROL SSH_AUTH_SOCK
dbus-update-activation-environment --all >/dev/null 2>&1 || true
# --password-store=gnome-libsecret forces Electron onto the libsecret backend instead of
# falling back to plaintext (and warning that the sign-in will not be saved). Headroom is
# intentionally not injected into the Desktop process: Claude Desktop force-overrides
# ANTHROPIC_BASE_URL (headroom #869), so Desktop uses the registered Headroom MCP tools.
exec claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret
# falling back to plaintext (and warning that the sign-in will not be saved).
CLAUDE_DESKTOP_COMMAND_FILE="/tmp/claude-desktop-command"
DEFAULT_CLAUDE_DESKTOP_COMMAND="claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret"
if [ -s "$CLAUDE_DESKTOP_COMMAND_FILE" ]; then
CLAUDE_DESKTOP_COMMAND="$(cat "$CLAUDE_DESKTOP_COMMAND_FILE")"
else
CLAUDE_DESKTOP_COMMAND="$DEFAULT_CLAUDE_DESKTOP_COMMAND"
fi
# Headroom is intentionally not injected into the Desktop process. Claude Desktop overrides
# ANTHROPIC_BASE_URL, so Desktop uses the registered Headroom MCP tools instead. The Claude Code
# terminal uses the supported `headroom wrap claude` integration through claude-headroom.
# Launch the configured command. If a custom/wrapped command fails to start, fall back to
# the plain Claude Desktop launch so the app always comes up for the user.
if ! sh -c "$CLAUDE_DESKTOP_COMMAND"; then
if [ "$CLAUDE_DESKTOP_COMMAND" != "$DEFAULT_CLAUDE_DESKTOP_COMMAND" ]; then
echo "autostart: '$CLAUDE_DESKTOP_COMMAND' failed; falling back to default Claude Desktop launch" >&2
exec sh -c "$DEFAULT_CLAUDE_DESKTOP_COMMAND"
fi
fi

View File

@@ -1,4 +1,4 @@
# Hourly RTK + Headroom + TokenSave savings report to the add-on log.
# Hourly rtk + headroom token-savings report to the add-on log (heartbeat + gains).
# 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,35 +3,9 @@
# shellcheck disable=SC2046
set -e
# Align the shared desktop user (abc) with the configured PUID/PGID before any storage is
# chowned and before any service or s6-setuidgid call resolves abc. The base image's
# init-adduser applies the same remap, but it runs after cont-init, so doing it here first is
# what lets the tokensave/rtk/git setup in the 8x scripts run under the final identity.
PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else echo '1000'; fi)"
PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '1000'; fi)"
# Claude Code refuses bypass-permissions mode under an effective root UID, so bypass mode
# always needs a non-root desktop user.
if [ "$(bashio::config 'permission_mode')" = "bypass" ] && [ "$PUID" -eq 0 ]; then
bashio::log.warning "permission_mode: bypass cannot run Claude Code as root; using UID 1000 instead of the configured PUID 0"
PUID=1000
fi
groupmod -o -g "$PGID" abc 2> /dev/null || true
usermod -o -u "$PUID" abc 2> /dev/null || true
if [ "$(id -u abc)" -ne "$PUID" ] || [ "$(id -g abc)" -ne "$PGID" ]; then
PUID="$(id -u abc)"
PGID="$(id -g abc)"
bashio::log.warning "Unable to remap the abc desktop user; continuing with its current identity ${PUID}:${PGID}"
fi
# The base image's init-adduser reads PUID/PGID from the raw add-on options (default 0) and
# runs mid-startup, racing the services. Pin it to the effective identity chosen above so it
# can never remap abc away from the ownership applied below.
ADDUSER_RUN="/etc/s6-overlay/s6-rc.d/init-adduser/run"
if [ -f "$ADDUSER_RUN" ]; then
sed -i "s|^PUID=.*|PUID=${PUID}|;s|^PGID=.*|PGID=${PGID}|" "$ADDUSER_RUN"
fi
# Define user
PUID=$(bashio::config "PUID")
PGID=$(bashio::config "PGID")
# Check data location
LOCATION="$(bashio::config 'data_location')"
@@ -85,23 +59,10 @@ printf "%s" "$LOCATION" > "$S6_ENVDIR/HOME"
printf "%s" "$LOCATION" > "$S6_ENVDIR/FM_HOME"
printf "%s" "/tmp/cache" > "$S6_ENVDIR/XDG_CACHE_HOME"
printf "%s" "$XDG_RUNTIME_DIR" > "$S6_ENVDIR/XDG_RUNTIME_DIR"
# Re-derived on every boot rather than gated on a "does it already say $LOCATION" grep: that
# guard only ever recognized the CURRENT $LOCATION, so a user who changed data_location and
# later changed it back left two stale HOME/FM_HOME exports in ~/.bashrc, with the last one
# (not necessarily the correct one) winning for every interactive shell. The marker makes this
# idempotent regardless of how many times $LOCATION has changed: strip any previously managed
# block, then append one that reflects the current value.
BASHRC_HOME_BEGIN="# --- BEGIN ADDON HOME (managed) ---"
BASHRC_HOME_END="# --- END ADDON HOME (managed) ---"
if [ -f ~/.bashrc ]; then
sed -i "/^${BASHRC_HOME_BEGIN}\$/,/^${BASHRC_HOME_END}\$/d" ~/.bashrc
fi
{
printf "%s\n" "$BASHRC_HOME_BEGIN"
grep -qxF "export HOME=\"$LOCATION\"" ~/.bashrc 2>/dev/null || {
printf "%s\n" "export HOME=\"$LOCATION\""
printf "%s\n" "export FM_HOME=\"$LOCATION\""
printf "%s\n" "export XDG_CACHE_HOME=\"/tmp/cache\""
printf "%s\n" "$BASHRC_HOME_END"
} >> ~/.bashrc
bashio::log.info "Creating $LOCATION"
@@ -109,11 +70,6 @@ mkdir -p "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR"
chmod 755 /tmp/cache
chmod 700 "$XDG_RUNTIME_DIR"
# /tmp is a tmpfs and Xorg runs as the non-root abc user, which cannot create the X11 socket
# directory itself (_XSERVTransmkdir: euid != 0). Pre-create it with the standard sticky mode.
mkdir -p /tmp/.X11-unix
chmod 1777 /tmp/.X11-unix
# Pre-create the Selkies joystick log so the base image's "chmod 777 /tmp/selkies*"
# calls (in init-selkies-config and svc-de) never fail on an empty glob.
touch /tmp/selkies_js.log
@@ -125,7 +81,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" /data
chown -R "$PUID":"$PGID" "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR"
chmod -R 700 "$LOCATION"
# The base init-selkies-config script overrides XDG_RUNTIME_DIR to $HOME/.XDG, which lands

View File

@@ -1,52 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# Grant the shared desktop user (abc) access to the exposed GPU render nodes *before* the
# graphical services start.
#
# The LinuxServer base image already sets up /dev/dri group access, but it does so in its
# init-video s6 oneshot, which is NOT a dependency of svc-xorg/svc-selkies/svc-de. On Home
# Assistant those long-running services routinely start (as abc, via s6-setuidgid) before
# init-video has added abc to the render group, so Xorg/Selkies/pixelflux open the render
# device with the wrong credentials and fail:
#
# libEGL warning: failed to open /dev/dri/card0: Permission denied
#
# With no usable render node the video pipeline never produces frames, so the Selkies web
# client stays on "waiting for stream" indefinitely and Claude Desktop never appears.
#
# cont-init.d runs to completion before any s6-rc service is started, so preparing the DRI
# nodes here wins that race. Everything is best-effort: a host that exposes no GPU simply has
# no nodes to touch and this is a no-op.
shopt -s nullglob
dri_nodes=(/dev/dri/card* /dev/dri/render*)
if [ "${#dri_nodes[@]}" -eq 0 ]; then
bashio::log.info "No /dev/dri render nodes exposed; skipping GPU permission setup"
exit 0
fi
for node in "${dri_nodes[@]}"; do
[ -e "$node" ] || continue
# Mirror the base image's init-video logic (add abc to the node's owning group, creating
# the group when the GID is unnamed) but early enough that the s6-setuidgid at service
# start picks the membership up.
gid="$(stat -c '%g' "$node")"
gname="$(getent group "$gid" | awk -F: '{print $1}')"
if [ -z "$gname" ]; then
gname="dri${gid}"
groupadd -o -g "$gid" "$gname" 2> /dev/null || true
fi
if ! id -G abc 2> /dev/null | tr ' ' '\n' | grep -qx "$gid"; then
usermod -a -G "$gname" abc 2> /dev/null || true
fi
# Guarantee access even where group propagation is unreliable inside the add-on sandbox:
# this is a single-user desktop container, so world read/write on the local render node
# is acceptable and removes any dependency on group-membership timing.
chmod o+rw "$node" 2> /dev/null || true
bashio::log.info "GPU: prepared ${node} (group ${gname}:${gid}) for the desktop user"
done

View File

@@ -1,48 +1,53 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
# shellcheck disable=SC2015
set -e
# The image is Debian-based (apt) and always ships uv, so those are the only installers used.
if bashio::config.has_value 'additional_apps'; then
bashio::log.info "Installing additional apps :"
apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null || bashio::log.warning "Unable to update apt package lists"
for packagestoinstall in $(bashio::config 'additional_apps' | tr ',' ' '); do
NEWAPPS=$(bashio::config 'additional_apps')
if command -v "apt-get" &> /dev/null; then
apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null || bashio::log.warning "Unable to update apt package lists"
fi
for packagestoinstall in ${NEWAPPS//,/ }; do
bashio::log.green "... $packagestoinstall"
apt-get install -yqq --no-install-recommends "$packagestoinstall" &> /dev/null || bashio::log.fatal "Error : $packagestoinstall not found"
if command -v "apk" &> /dev/null; then
apk add --no-cache "$packagestoinstall" &> /dev/null || (bashio::log.fatal "Error : $packagestoinstall not found")
elif command -v "apt-get" &> /dev/null; then
apt-get install -yqq --no-install-recommends "$packagestoinstall" &> /dev/null || (bashio::log.fatal "Error : $packagestoinstall not found")
elif command -v "pacman" &> /dev/null; then
pacman --noconfirm -S "$packagestoinstall" &> /dev/null || (bashio::log.fatal "Error : $packagestoinstall not found")
fi
done
fi
if bashio::config.has_value 'additional_pip'; then
for p in $(bashio::config 'additional_pip' | tr ',' ' '); do
bashio::log.green "... pip: $p"
uv pip install --system --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed"
pip3 install --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed"
done
fi
if bashio::config.has_value 'TZ'; then
TIMEZONE=$(bashio::config 'TZ')
if [ -f "/usr/share/zoneinfo/$TIMEZONE" ]; then
bashio::log.info "Setting timezone to $TIMEZONE"
ln -snf "/usr/share/zoneinfo/$TIMEZONE" /etc/localtime
echo "$TIMEZONE" > /etc/timezone
else
bashio::log.fatal "Error : $TIMEZONE not found. Here is a list of valid timezones : https://manpages.ubuntu.com/manpages/focal/man3/DateTime::TimeZone::Catalog.3pm.html"
fi
fi
bashio::log.info "Setting timezone to $TIMEZONE"
ln -snf /usr/share/zoneinfo/"$TIMEZONE" /etc/localtime
echo "$TIMEZONE" > /etc/timezone
fi || (bashio::log.fatal "Error : $TIMEZONE not found. Here is a list of valid timezones : https://manpages.ubuntu.com/manpages/focal/man3/DateTime::TimeZone::Catalog.3pm.html")
if bashio::config.has_value 'KEYBOARD'; then
KEYBOARD=$(bashio::config 'KEYBOARD')
bashio::log.info "Setting keyboard to $KEYBOARD"
if [ -d /var/run/s6/container_environment ]; then printf "%s" "$KEYBOARD" > /var/run/s6/container_environment/KEYBOARD; fi
grep -qxF "KEYBOARD=\"$KEYBOARD\"" ~/.bashrc 2> /dev/null || printf "%s\n" "KEYBOARD=\"$KEYBOARD\"" >> ~/.bashrc
fi
grep -qxF "KEYBOARD=\"$KEYBOARD\"" ~/.bashrc 2>/dev/null || printf "%s\n" "KEYBOARD=\"$KEYBOARD\"" >> ~/.bashrc
fi || true
if bashio::config.has_value 'PASSWORD'; then
bashio::log.info "Setting password to the value defined in options"
PASSWORD=$(bashio::config 'PASSWORD')
passwd -d abc
echo -e "$PASSWORD\n$PASSWORD" | passwd abc
elif [[ -n "$(bashio::addon.port "3000")" ]] || [[ -n "$(bashio::addon.port "3001")" ]]; then
elif ! bashio::config.has_value 'PASSWORD' && { [[ -n "$(bashio::addon.port "3000")" ]] || [[ -n "$(bashio::addon.port "3001")" ]] }; then
bashio::log.warning "SEVERE RISK IDENTIFIED"
bashio::log.warning "You are opening an external port but your password is not defined"
bashio::log.warning "You risk being hacked ! Please disable the external ports, or use a password"

View File

@@ -2,9 +2,11 @@
# shellcheck shell=bash
set -e
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"
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
fi

View File

@@ -3,414 +3,80 @@
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)"
mkdir -p "$HOME/.claude"
CLAUDE_MD="$HOME/.claude/CLAUDE.md"
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"
# Managed, idempotent guidance block in the user's global CLAUDE.md, delimited by
# "<!-- BEGIN/END <name> (managed by claude_desktop addon) -->" markers. `add` appends the
# block (body on stdin) unless the marker is already present; `remove` strips the whole
# block, surrounding blank padding included, and leaves everything else untouched.
manage_claude_md_block() {
local name="$1" action="$2"
local begin="<!-- BEGIN ${name} (managed by claude_desktop addon) -->"
if [ "$action" = "add" ]; then
if ! { [ -f "$CLAUDE_MD" ] && grep -qF "$begin" "$CLAUDE_MD"; }; then
bashio::log.info "Adding ${name} guidance to CLAUDE.md"
mkdir -p "$(dirname "$CLAUDE_MD")"
{
[ -s "$CLAUDE_MD" ] && printf '\n'
printf '%s\n' "$begin"
cat
printf '%s\n' "<!-- END ${name} (managed by claude_desktop addon) -->"
} >> "$CLAUDE_MD"
fi
elif [ -f "$CLAUDE_MD" ] && grep -qF "$begin" "$CLAUDE_MD"; then
bashio::log.info "Removing ${name} guidance from CLAUDE.md"
CLAUDE_MD="$CLAUDE_MD" BLOCK_NAME="$name" python3 - <<'PY' || bashio::log.warning "Unable to remove the ${name} guidance automatically"
import os
import re
from pathlib import Path
path = Path(os.environ["CLAUDE_MD"])
name = re.escape(os.environ["BLOCK_NAME"])
text = path.read_text(encoding="utf-8")
pattern = re.compile(
rf"\n*<!-- BEGIN {name} \(managed by claude_desktop addon\) -->.*?"
rf"<!-- END {name} \(managed by claude_desktop addon\) -->\n?",
re.DOTALL,
)
new = pattern.sub("", text)
if new != text:
path.write_text(new, encoding="utf-8")
PY
fi
}
# Managed hook entry in ~/.claude/settings.json (settings hooks apply to terminal, cowork,
# dispatch and cron sessions alike). The managed command is stripped everywhere first and
# re-appended when adding, so one pass handles removal, de-duplication, and matcher migration
# on upgrades; hooks owned by other tools (e.g. tokensave's own entries) are preserved, and
# the final text comparison keeps the write idempotent across boots.
manage_settings_hook() {
# manage_settings_hook <event> <matcher> <command> <add|remove>
HOOK_EVENT="$1" HOOK_MATCHER="$2" HOOK_COMMAND="$3" HOOK_ACTION="$4" \
python3 - <<'PY' || bashio::log.warning "Unable to update the $1 hook for '$3'"
# 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. Register it in Claude Desktop's MCP config, leaving the
# plain launch untouched. The merge is idempotent and preserves any other MCP servers.
CLAUDE_DESKTOP_CONFIG="$HOME/.config/Claude/claude_desktop_config.json"
if bashio::config.true 'install_headroom'; then
if command -v headroom &> /dev/null; then
bashio::log.info "headroom $(headroom --version 2> /dev/null || true) available; registering the headroom MCP server for Claude Desktop"
HEADROOM_BIN="$(command -v headroom)" CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" python3 - <<'PY' || bashio::log.warning "Unable to register the headroom MCP server automatically"
import json
import os
from pathlib import Path
event = os.environ["HOOK_EVENT"]
matcher = os.environ["HOOK_MATCHER"]
command = os.environ["HOOK_COMMAND"]
action = os.environ["HOOK_ACTION"]
path = Path.home() / ".claude" / "settings.json"
original = path.read_text() if path.exists() else None
if original is None and action != "add":
raise SystemExit(0)
path = Path(os.environ["CLAUDE_DESKTOP_CONFIG"])
try:
data = json.loads(original) if original is not None else {}
data = json.loads(path.read_text()) if path.exists() else {}
if not isinstance(data, dict):
data = {}
except Exception:
if action != "add":
raise SystemExit(0)
path.rename(path.with_suffix(path.suffix + ".bak"))
original = None
if path.exists():
path.rename(path.with_suffix(path.suffix + ".bak"))
data = {}
hooks = data.get("hooks") if isinstance(data.get("hooks"), dict) else {}
entries = hooks.get(event) if isinstance(hooks.get(event), list) else []
filtered = []
for entry in entries:
if not isinstance(entry, dict) or not isinstance(entry.get("hooks"), list):
filtered.append(entry)
continue
kept = [
item
for item in entry["hooks"]
if not (isinstance(item, dict) and item.get("command") == command)
]
if len(kept) != len(entry["hooks"]):
if not kept:
continue
entry = dict(entry)
entry["hooks"] = kept
filtered.append(entry)
entries = filtered
if action == "add":
entries.append({"matcher": matcher, "hooks": [{"type": "command", "command": command}]})
if entries:
hooks[event] = entries
else:
hooks.pop(event, None)
if hooks:
data["hooks"] = hooks
else:
data.pop("hooks", None)
serialized = json.dumps(data, indent=2) + "\n"
if serialized != original:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(serialized)
servers = data.get("mcpServers")
if not isinstance(servers, dict):
servers = {}
data["mcpServers"] = servers
servers["headroom"] = {"command": os.environ.get("HEADROOM_BIN", "headroom"), "args": ["mcp", "serve"]}
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, indent=2) + "\n")
PY
}
# Headroom'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).
# The merge is idempotent, preserves any other MCP servers, never overwrites a user-customized
# entry with a different command, and removes only add-on-managed entries when disabled.
CLAUDE_DESKTOP_CONFIG="$HOME/.config/Claude/claude_desktop_config.json"
CLAUDE_CODE_CONFIG="$HOME/.claude.json"
HEADROOM_ENABLED=false
if bashio::config.true 'install_headroom'; then
if command -v headroom &> /dev/null; then
HEADROOM_ENABLED=true
bashio::log.info "headroom $(headroom --version 2> /dev/null || true) available; registering the headroom MCP server"
else
bashio::log.warning "headroom is not available"
fi
fi
TOKENSAVE_ENABLED=false
if bashio::config.true 'install_tokensave'; then
if command -v tokensave &> /dev/null; then
TOKENSAVE_ENABLED=true
bashio::log.info "tokensave $(tokensave --version 2> /dev/null || true) available; 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
# 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.
# Runs before the MCP registration merge below on purpose: a first-time `tokensave init` also
# rewrites ~/.claude.json itself (at default permissions), and the merge afterwards reconciles
# the managed entries and re-tightens the file mode around the stored HA token.
if $TOKENSAVE_ENABLED; then
declare -A TOKENSAVE_REPOS_SEEN=()
# Capture the list BEFORE looping: bashio::config's internals trip the errexit that
# process substitution inherits from the bashio wrapper (a `read -d ''` that always
# returns non-zero), so `done < <(bashio::config ...)` silently fed the loop an EMPTY
# list — the startup index/sync never ran. Command substitution runs without errexit
# (inherit_errexit is off), making this form reliable. bashio::config prints list
# entries one per line, without a trailing newline and as "null" when the key is absent
# (bashio::config.array only exists in the repo's standalone bashio, not the real one
# here); the `|| [ -n ... ]` test keeps the final unterminated record in the loop.
TOKENSAVE_PROJECT_PATHS="$(bashio::config 'tokensave_project_paths')"
while IFS= read -r configured_path || [ -n "$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:]]}"}"
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
continue
fi
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
# The one-shot safe.directory override is used only to discover the repository root.
repo_root="$(run_as_runtime_user git -c safe.directory='*' -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
# Persist the resolved root in the runtime user's Git config so the sync/init below,
# tokensave's git hooks, and Claude sessions all pass Git's dubious-ownership check.
if ! run_as_runtime_user git config --global --get-all safe.directory \
| grep -Fxq -- "$repo_root"; then
run_as_runtime_user git config --global --add safe.directory "$repo_root"
bashio::log.info "Marked TokenSave repository as safe for Git: ${repo_root}"
fi
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); waits up to 60s for the
# other writer to finish rather than silently skipping, since a held lock clears
# itself the moment its holder exits or dies (the kernel releases flock on exit);
# * an existing index is refreshed with a cheap incremental `sync`, retried a few
# times because SQLITE_BUSY under lock contention is transient, not corruption;
# * quarantine is reserved for sync failures whose stderr actually names database
# corruption (SQLite's own "malformed"/"not a database"/"disk image" wording) or
# a half-written index from an interrupted `init` (sentinel-flagged). Any other
# failure (permissions, disk full, missing binary, ...) leaves the existing index
# untouched and simply retries on the next start — corruption should self-heal,
# a transient environment problem should not nuke a healthy graph;
# * `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 the startup 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 -w 60 9; then
echo "TokenSave: index still locked for $repo_root after 60s; skipping startup sync" >&2
exit 0
fi
is_corruption() {
printf "%s" "$1" | grep -qiE "malformed|not a database|file is encrypted|disk image|database.*corrupt"
}
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
sync_err="$(tokensave sync "$repo_root" 2>&1 1>/dev/null)" && exit 0
[ "$attempt" -ge 3 ] && break
echo "TokenSave: sync attempt $attempt failed for $repo_root; retrying" >&2
attempt=$((attempt + 1))
sleep 2
done
if is_corruption "$sync_err"; then
echo "TokenSave: sync failed after retries for $repo_root (corruption detected); rebuilding index" >&2
quarantine
else
echo "TokenSave: sync failed after retries for $repo_root (no corruption signature); leaving index in place, will retry next start" >&2
echo "TokenSave: last sync error: $sync_err" >&2
exit 1
fi
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 <<< "$TOKENSAVE_PROJECT_PATHS"
fi
HA_MCP_ENABLED=false
HA_MCP_URL=""
HA_MCP_TOKEN=""
if bashio::config.true 'enable_ha_mcp'; then
HA_MCP_URL="$(bashio::config 'ha_mcp_url' 'http://homeassistant:8123/api/mcp')"
if bashio::config.has_value 'ha_mcp_token'; then
HA_MCP_TOKEN="$(bashio::config 'ha_mcp_token')"
fi
if [ -z "$HA_MCP_TOKEN" ]; then
bashio::log.warning "enable_ha_mcp is on but ha_mcp_token is empty; set a Home Assistant long-lived access token (Profile -> Security) and enable the 'Model Context Protocol Server' integration"
elif ! command -v mcp-proxy &> /dev/null; then
bashio::log.warning "mcp-proxy is not available; cannot register the Home Assistant MCP server"
else
HA_MCP_ENABLED=true
bashio::log.info "Registering the Home Assistant MCP server (${HA_MCP_URL})"
fi
fi
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
HEADROOM_HF_HOME="${HOME}/.headroom/hf" \
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \
HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
python3 - <<'PY' || bashio::log.warning "Unable to update the MCP server registrations automatically"
elif [ -f "$CLAUDE_DESKTOP_CONFIG" ]; then
bashio::log.info "Removing the headroom MCP server from Claude Desktop"
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" python3 - <<'PY' || bashio::log.warning "Unable to remove the headroom MCP server automatically"
import json
import os
from pathlib import Path
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", "--proxy-url", "http://127.0.0.1:8787"],
# The MCP server is a separate process from the svc-headroom proxy longrun and does
# not inherit its HF_HOME export, so Kompress falls back to the default (tmpfs, wiped
# every restart) cache dir, never finds the model, and silently no-ops every
# compression request. Point it at the same persistent cache the proxy warms.
"env": {"HF_HOME": os.environ["HEADROOM_HF_HOME"]},
}
if os.environ["TOKENSAVE_ENABLED"] == "true":
desired["tokensave"] = {"command": os.environ["TOKENSAVE_BIN"], "args": ["serve"]}
if os.environ["HA_MCP_ENABLED"] == "true":
# Home Assistant's MCP Server integration speaks stateless Streamable HTTP on /api/mcp;
# mcp-proxy defaults to SSE, so the transport flags are required.
desired["homeassistant"] = {
"command": os.environ["MCP_PROXY_BIN"],
"args": ["--transport=streamablehttp", "--stateless", os.environ["HA_MCP_URL"]],
"env": {"API_ACCESS_TOKEN": os.environ["HA_MCP_TOKEN"]},
}
# An entry is add-on-managed when its command is one of our binaries living outside the
# persistent home. Matching on the basename (rather than the exact path recorded at write
# time) keeps entries updatable when a base-image upgrade moves the binary, while commands
# under $HOME stay untouched because those are user-installed.
HOME_PREFIX = os.path.expanduser("~") + os.sep
def is_managed(name, entry):
if not isinstance(entry, dict):
return False
command = entry.get("command")
if not isinstance(command, str) or command.startswith(HOME_PREFIX):
return False
return os.path.basename(command) == MANAGED_BASENAMES[name]
for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_CONFIG", True)):
path = Path(os.environ[config_var])
try:
data = json.loads(path.read_text()) if path.exists() else {}
if not isinstance(data, dict):
data = {}
except Exception:
if path.exists():
path.rename(path.with_suffix(path.suffix + ".bak"))
data = {}
path = Path(os.environ["CLAUDE_DESKTOP_CONFIG"])
data = json.loads(path.read_text())
if isinstance(data, dict):
servers = data.get("mcpServers")
if not isinstance(servers, dict):
servers = {}
changed = False
for name in MANAGED_BASENAMES:
existing = servers.get(name)
if name in desired:
entry = dict(desired[name])
if stdio_type:
entry["type"] = "stdio"
if existing is None or is_managed(name, existing):
if existing != entry:
servers[name] = entry
changed = True
elif existing is not None and is_managed(name, existing):
del servers[name]
changed = True
if changed:
if servers:
data["mcpServers"] = servers
else:
if isinstance(servers, dict) and servers.pop("headroom", None) is not None:
if not servers:
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. Enforced even
# on no-change boots because tokensave's own writes can recreate the file with default
# permissions between merges.
if path.exists():
path.chmod(0o600)
PY
fi
# Guide Claude to actually use the Headroom compression tools so the MCP integration produces
# real savings when transparent proxying is unavailable.
if $HEADROOM_ENABLED; then
manage_claude_md_block headroom add <<'MD'
# 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.
CLAUDE_MD="$HOME/.claude/CLAUDE.md"
HEADROOM_GUIDE_BEGIN="<!-- BEGIN headroom (managed by claude_desktop addon) -->"
if bashio::config.true 'install_headroom'; 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"
{
[ -s "$CLAUDE_MD" ] && printf '\n'
cat <<'MD'
<!-- BEGIN headroom (managed by claude_desktop addon) -->
## Headroom context compression
A local Headroom proxy (127.0.0.1:8787) backs the `headroom` MCP tools. To save context tokens:
@@ -420,32 +86,40 @@ search results, JSON/config dumps, big command outputs, roughly >500 tokens —
the raw content. Call `mcp__headroom__headroom_retrieve` with that hash when you need the full
original back. Skip compression for error/stack-trace output (Headroom deliberately protects it)
and for small or one-off content. Use `mcp__headroom__headroom_stats` to check savings.
<!-- END headroom (managed by claude_desktop addon) -->
MD
else
manage_claude_md_block headroom remove
fi
# Route every Claude Code session through the Headroom proxy via the `env` block in the user's
# ~/.claude/settings.json. Claude Code writes settings `env` entries into the process
# environment at startup, replacing inherited values — this is the only supported way to reach
# Desktop cowork/local-agent-mode sessions, which spawn the bundled CLI at an absolute path
# (bypassing the PATH wrapper) with ANTHROPIC_BASE_URL pinned to the production endpoint
# (headroom #869). Managed-value semantics: only set or remove the variable when it is absent
# or already equals the add-on-managed proxy URL, so a user-customized endpoint is never
# clobbered. The svc-headroom longrun is s6-supervised, so a crashed proxy restarts within
# seconds; the terminal wrapper's per-launch health check remains as an extra safety net.
if $HEADROOM_ENABLED && bashio::config.true 'headroom_wrap_claude_code'; then
HEADROOM_ROUTE_ACTION="add"
else
HEADROOM_ROUTE_ACTION="remove"
fi
HEADROOM_ROUTE_ACTION="$HEADROOM_ROUTE_ACTION" python3 - <<'PY' || bashio::log.warning "Unable to manage the Claude Code proxy routing env"
import json
} >> "$CLAUDE_MD"
fi
elif [ -f "$CLAUDE_MD" ] && grep -qF "$HEADROOM_GUIDE_BEGIN" "$CLAUDE_MD"; then
bashio::log.info "Removing headroom usage guidance from CLAUDE.md"
CLAUDE_MD="$CLAUDE_MD" python3 - <<'PY' || bashio::log.warning "Unable to remove headroom guidance automatically"
import os
import re
from pathlib import Path
MANAGED_URL = "http://127.0.0.1:8787"
path = Path(os.environ["CLAUDE_MD"])
text = path.read_text()
pattern = re.compile(
r"\n*<!-- BEGIN headroom \(managed by claude_desktop addon\) -->.*?"
r"<!-- END headroom \(managed by claude_desktop addon\) -->\n?",
re.DOTALL,
)
new = pattern.sub("", text)
if new != text:
path.write_text(new)
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"
import json
from pathlib import Path
path = Path.home() / ".claude" / "settings.json"
try:
data = json.loads(path.read_text()) if path.exists() else {}
@@ -455,96 +129,79 @@ except Exception:
if path.exists():
path.rename(path.with_suffix(path.suffix + ".bak"))
data = {}
env = data.get("env")
if not isinstance(env, dict):
env = {}
current = env.get("ANTHROPIC_BASE_URL")
changed = False
if os.environ["HEADROOM_ROUTE_ACTION"] == "add":
if current is None or current == MANAGED_URL:
if current != MANAGED_URL:
env["ANTHROPIC_BASE_URL"] = MANAGED_URL
changed = True
else:
print(f"Claude settings env already sets ANTHROPIC_BASE_URL={current}; leaving it untouched")
elif current == MANAGED_URL:
del env["ANTHROPIC_BASE_URL"]
changed = True
if changed:
if env:
data["env"] = env
elif "env" in data:
del data["env"]
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, indent=2) + "\n")
hooks = data.setdefault("hooks", {})
pre = hooks.setdefault("PreToolUse", [])
rtk_entry = {"matcher": "Bash", "hooks": [{"type": "command", "command": "rtk hook claude"}]}
if not any("rtk hook claude" in json.dumps(entry) for entry in pre if isinstance(entry, dict)):
pre.append(rtk_entry)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, indent=2) + "\n")
PY
# Compress large tool outputs automatically in every Claude Code session via a managed
# PostToolUse hook. Desktop-spawned sessions pin ANTHROPIC_BASE_URL to the production endpoint
# (headroom #869) so the proxy never sees their traffic, and the CLAUDE.md guidance above only
# helps when the model remembers to call the MCP tools. The hook closes that gap: outputs over
# ~4000 chars from Bash/Grep/Glob/WebFetch are compressed with Headroom's rule-based pipeline
# and swapped in through hookSpecificOutput.updatedToolOutput, with the original kept in the
# shared CCR store so the model can fetch it back with mcp__headroom__headroom_retrieve. The
# script fails open (any error leaves the tool output untouched) and its --self-test gate
# keeps a broken interpreter path from registering a hook that would warn on every tool call.
HEADROOM_HOOK_CMD="/usr/local/bin/headroom-posttooluse-compress.py"
HEADROOM_HOOK_ACTION="remove"
if $HEADROOM_ENABLED && bashio::config.true 'headroom_auto_compress'; then
if run_as_runtime_user "$HEADROOM_HOOK_CMD" --self-test; then
HEADROOM_HOOK_ACTION="add"
bashio::log.info "Registering the Headroom PostToolUse auto-compression hook"
else
bashio::log.warning "headroom-posttooluse-compress.py --self-test failed; not registering the auto-compression hook"
fi
fi
manage_settings_hook PostToolUse "Bash|Grep|Glob|WebFetch" "$HEADROOM_HOOK_CMD" "$HEADROOM_HOOK_ACTION"
# Tell Claude Code that it can configure Home Assistant over the Core API via the shipped
# `ha-cli` helper (no /config filesystem mount needed).
if bashio::config.true 'enable_ha_api_helper'; then
manage_claude_md_block ha-api-helper add <<'MD'
## 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`).
MD
else
manage_claude_md_block ha-api-helper remove
fi
if bashio::config.true 'install_rtk'; then
if command -v rtk &> /dev/null; then
bashio::log.info "Configuring rtk Claude Code integration"
# `rtk init -g` writes ~/.claude/RTK.md and its @RTK.md include in CLAUDE.md, but in
# non-interactive mode it deliberately refuses to patch settings.json, so the hook
# entry that actually rewrites Bash commands is registered here.
run_as_runtime_user env RTK_NONINTERACTIVE=1 rtk init -g \
|| bashio::log.warning "rtk global files configuration failed"
manage_settings_hook PreToolUse Bash "rtk hook claude" add
fi
else
bashio::log.warning "rtk is not available"
fi
else
manage_settings_hook PreToolUse Bash "rtk hook claude" remove
elif [ -f "$HOME/.claude/settings.json" ]; then
bashio::log.info "Removing the add-on-managed rtk Claude Code hook"
python3 - <<'PY' || bashio::log.warning "Unable to remove rtk hook automatically"
import json
from pathlib import Path
path = Path.home() / ".claude" / "settings.json"
data = json.loads(path.read_text())
if not isinstance(data, dict):
raise TypeError("Claude settings must contain a JSON object")
hooks = data.get("hooks")
if not isinstance(hooks, dict):
raise SystemExit(0)
entries = hooks.get("PreToolUse")
if not isinstance(entries, list):
raise SystemExit(0)
changed = False
filtered_entries = []
for entry in entries:
if not isinstance(entry, dict) or entry.get("matcher") != "Bash":
filtered_entries.append(entry)
continue
commands = entry.get("hooks")
if not isinstance(commands, list):
filtered_entries.append(entry)
continue
filtered_commands = [
command
for command in commands
if not (
isinstance(command, dict)
and command.get("type") == "command"
and command.get("command") == "rtk hook claude"
)
]
if len(filtered_commands) == len(commands):
filtered_entries.append(entry)
continue
changed = True
if filtered_commands:
updated_entry = dict(entry)
updated_entry["hooks"] = filtered_commands
filtered_entries.append(updated_entry)
if changed:
if filtered_entries:
hooks["PreToolUse"] = filtered_entries
else:
hooks.pop("PreToolUse", None)
if hooks:
data["hooks"] = hooks
else:
data.pop("hooks", None)
path.write_text(json.dumps(data, indent=2) + "\n")
PY
fi
if bashio::config.true 'install_caveman'; then
@@ -552,13 +209,17 @@ 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"
find "$HOME/.claude" -maxdepth 4 -iname '*caveman*' -exec rm -rf {} + 2> /dev/null || true
fi
# Ownership of everything written above is reconciled by 84-claude_runtime_ownership.sh after
# the remaining Claude configuration scripts have run.
# Startup configuration runs as root, while Claude Desktop and the web terminal run as abc.
# Return managed persistent files to the configured runtime UID/GID after all writes complete.
for managed_path in "$HOME/.claude" "$HOME/.config/Claude"; do
if [ -e "$managed_path" ]; then
chown -R -- "${PUID}:${PGID}" "$managed_path" || bashio::log.warning "Unable to set ownership on $managed_path"
fi
done

View File

@@ -1,73 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
set -o pipefail
PERMISSION_MODE="$(bashio::config 'permission_mode')"
SETTINGS_PATH="$HOME/.claude/settings.json"
case "$PERMISSION_MODE" in
strict | auto | bypass) ;;
*)
bashio::log.warning "Unknown permission_mode '${PERMISSION_MODE}'; falling back to strict"
PERMISSION_MODE="strict"
;;
esac
# Managed-value semantics, matching the ANTHROPIC_BASE_URL handling in 82-claude_tools.sh:
# auto/bypass set permissions.defaultMode to the add-on-managed value, and strict removes it
# only while it still holds one of those managed values — a defaultMode the user set by hand
# is never deleted. Ownership of the written file is reconciled by 84-claude_runtime_ownership.sh.
mkdir -p "$(dirname "$SETTINGS_PATH")"
PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" python3 - <<'PY'
import json
import os
from pathlib import Path
MANAGED_VALUES = {"auto", "bypassPermissions"}
mode = os.environ["PERMISSION_MODE"]
settings_path = Path(os.environ["SETTINGS_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 = {}
permissions = settings.get("permissions")
if not isinstance(permissions, dict):
permissions = {}
if mode == "strict":
if permissions.get("defaultMode") in MANAGED_VALUES:
permissions.pop("defaultMode")
else:
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
# Drop the state file older add-on versions used to remember the pre-add-on defaultMode.
rm -f "$HOME/.claude/.addon-permission-mode.json"
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

View File

@@ -17,54 +17,25 @@ if ! command -v gh > /dev/null 2>&1; then
exit 0
fi
# Everything below writes into the abc runtime user's HOME, so it must run AS abc. cont-init
# runs as root with HOME already pointing at the persistent data location, so plain
# `git config --global` recreated ~/.gitconfig owned by root:root on every start — and because
# that file is rewritten each boot, 20-folders.sh's earlier recursive chown never stuck to it.
# The user who actually runs git, gh and Claude was then unable to read its own committer
# identity or the gh credential helper, so every commit failed with "Author identity unknown"
# and authenticated pushes fell back to prompting. 20-folders.sh already remapped abc to the
# effective runtime identity (never root in bypass mode), so follow abc rather than re-reading
# the raw PUID/PGID options here.
RUNTIME_UID="$(id -u abc)"
RUNTIME_GID="$(id -g abc)"
run_as_runtime_user() {
s6-setuidgid abc env HOME="$HOME" "$@"
}
# Reclaim any root-owned copies left by an earlier add-on version before writing as abc:
# these paths are not covered by 82-claude_tools.sh's ownership pass, and a root-owned
# ~/.gitconfig would make the first `git config` below fail outright under `set -e`.
mkdir -p "$HOME/.config"
chown -- "${RUNTIME_UID}:${RUNTIME_GID}" "$HOME/.config"
for managed_path in "$HOME/.gitconfig" "$HOME/.config/gh"; do
if [ -e "$managed_path" ]; then
chown -R -- "${RUNTIME_UID}:${RUNTIME_GID}" "$managed_path" || bashio::log.warning "Unable to set ownership on $managed_path"
fi
done
if bashio::config.has_value 'github_username'; then
run_as_runtime_user git config --global user.name "$(bashio::config 'github_username')"
git config --global user.name "$(bashio::config 'github_username')"
fi
if bashio::config.has_value 'github_email'; then
run_as_runtime_user git config --global user.email "$(bashio::config 'github_email')"
git config --global user.email "$(bashio::config 'github_email')"
fi
if bashio::config.has_value 'github_token'; then
token="$(bashio::config 'github_token')"
run_as_runtime_user mkdir -p "$HOME/.config/gh"
run_as_runtime_user chmod 700 "$HOME/.config/gh"
if run_as_runtime_user env -u GH_TOKEN -u GITHUB_TOKEN gh auth status --hostname github.com > /dev/null 2>&1; then
mkdir -p "$HOME/.config/gh"
chmod 700 "$HOME/.config/gh"
if env -u GH_TOKEN -u GITHUB_TOKEN gh auth status --hostname github.com > /dev/null 2>&1; then
bashio::log.info "GitHub CLI already authenticated for github.com"
else
bashio::log.info "Configuring GitHub CLI authentication for github.com"
printf '%s\n' "$token" | run_as_runtime_user env -u GH_TOKEN -u GITHUB_TOKEN gh auth login --hostname github.com --with-token || bashio::log.warning "GitHub CLI authentication failed"
printf '%s\n' "$token" | env -u GH_TOKEN -u GITHUB_TOKEN gh auth login --hostname github.com --with-token || bashio::log.warning "GitHub CLI authentication failed"
fi
run_as_runtime_user env -u GH_TOKEN -u GITHUB_TOKEN gh auth setup-git --hostname github.com || bashio::log.warning "GitHub CLI git credential setup failed"
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 "GitHub CLI available. Set github_token to authenticate gh and git operations."
fi

View File

@@ -1,17 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# Earlier configuration scripts intentionally run as root. 20-folders.sh remapped abc to the
# effective runtime identity (never root in bypass mode, where Claude Code refuses to run as
# root). Reconcile ownership with that identity after all Claude configuration writes are
# complete, as a safety net in case any intermediate step re-owned a managed path.
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

@@ -0,0 +1,101 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
set -o pipefail
declare port=7681
declare username
declare password=""
declare workspace
declare canonical_workspace
export PATH="${HOME:-/data/data}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
if bashio::config.has_value 'enable_terminal' && ! bashio::config.true 'enable_terminal'; then
bashio::log.info "svc-claude-terminal: terminal disabled; idling"
exec sleep infinity
fi
if [ -z "${HOME:-}" ]; then
bashio::log.error "svc-claude-terminal: HOME is not initialized; idling"
exec sleep infinity
fi
if ! command -v ttyd >/dev/null 2>&1 || ! command -v tmux >/dev/null 2>&1 || ! command -v claude >/dev/null 2>&1; then
bashio::log.error "svc-claude-terminal: ttyd, tmux, or Claude Code is missing; idling"
exec sleep infinity
fi
username="claude"
if bashio::config.has_value 'terminal_username'; then
username="$(bashio::config 'terminal_username')"
fi
if bashio::config.has_value 'terminal_password'; then
password="$(bashio::config 'terminal_password')"
elif bashio::config.has_value 'PASSWORD'; then
bashio::log.warning "svc-claude-terminal: using PASSWORD as fallback for terminal authentication; prefer a unique terminal_password"
password="$(bashio::config 'PASSWORD')"
fi
if [ -z "$password" ]; then
bashio::log.warning "svc-claude-terminal: set terminal_password (or PASSWORD) before mapping port ${port}; terminal will remain disabled"
exec sleep infinity
fi
workspace="${HOME}/workspace"
if bashio::config.has_value 'terminal_workspace'; then
workspace="$(bashio::config 'terminal_workspace')"
fi
if [[ "$workspace" != /* ]]; then
bashio::log.error "svc-claude-terminal: terminal_workspace must be an absolute path; idling"
exec sleep infinity
fi
if [ -L "$workspace" ]; then
bashio::log.error "svc-claude-terminal: terminal_workspace must not be a symbolic link; idling"
exec sleep infinity
fi
if ! canonical_workspace="$(realpath -m -- "$workspace")"; then
bashio::log.error "svc-claude-terminal: unable to resolve terminal_workspace '$workspace'; idling"
exec sleep infinity
fi
workspace="$canonical_workspace"
case "$workspace" in
"$HOME" | "$HOME"/* | /share/* | /media/* | /mnt/* | /data/* | /config/*)
;;
*)
bashio::log.error "svc-claude-terminal: terminal_workspace must be the configured data_location or a subdirectory of /share, /media, /mnt, /data, or /config; idling"
exec sleep infinity
;;
esac
if [ ! -e "$workspace" ]; then
if ! install -d -m 0750 -o abc -g abc -- "$workspace"; then
bashio::log.error "svc-claude-terminal: failed to create workspace '$workspace'; idling"
exec sleep infinity
fi
elif [ ! -d "$workspace" ]; then
bashio::log.error "svc-claude-terminal: terminal_workspace '$workspace' is not a directory; idling"
exec sleep infinity
fi
if ! s6-setuidgid abc test -r "$workspace" ||
! s6-setuidgid abc test -w "$workspace" ||
! s6-setuidgid abc test -x "$workspace"; then
bashio::log.error "svc-claude-terminal: workspace '$workspace' must be readable, writable, and searchable by user abc; idling"
exec sleep infinity
fi
export CLAUDE_TERMINAL_WORKSPACE="$workspace"
bashio::log.info "svc-claude-terminal: starting authenticated ttyd terminal on port ${port}; workspace=${workspace}"
exec s6-setuidgid abc ttyd \
-p "$port" \
-W \
-O \
-c "${username}:${password}" \
/usr/local/bin/claude-terminal-shell

View File

@@ -0,0 +1 @@
longrun

View File

@@ -3,28 +3,7 @@
declare port=8787
declare host=127.0.0.1
# 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
# Kompress (the ONNX compression engine) needs its model in the local HF cache: the
# proxy's startup preload is deliberately cache-only, and the default HF cache lands
# under ~/.cache, which the add-on points at tmpfs (/tmp/cache) — wiped on every
# restart. Without a warm persistent cache the proxy ran forever in "deferred" mode
# and recorded zero compression savings. Point the cache at persistent storage;
# nothing else is needed here — the proxy's own request path already downloads a
# missing model in the background on first use (ensure_background_load) and passes
# requests through uncompressed until it lands, so this self-heals within a couple of
# requests on the first boot and loads instantly (eager preload) on every boot after.
# A synchronous pre-warm was tried here and removed: it blocked the port bind for up
# to the download's duration, which left the settings-managed ANTHROPIC_BASE_URL
# (see 82-claude_tools.sh) pointing at a proxy that wasn't listening yet.
export HF_HOME="${HOME}/.headroom/hf"
mkdir -p "$HF_HOME"
chown abc:abc "$HF_HOME" 2> /dev/null || true
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

@@ -1,53 +0,0 @@
#!/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

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

View File

@@ -1,44 +1,34 @@
#!/usr/bin/with-contenv bashio
# 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
# 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
export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
if ! bashio::config.true 'enable_tools_health_report'; then
exit 0
fi
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
rtk_enabled=false
# 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
tokensave_enabled=false
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
if bashio::config.true 'install_headroom' && $have_headroom; 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
# Nothing to report if neither tool is active — stay quiet.
if ! $have_rtk && ! $headroom_enabled; then exit 0; fi
echo "===== claude tools report $(date '+%Y-%m-%d %H:%M:%S') ====="
echo "===== claude gains report $(date '+%Y-%m-%d %H:%M:%S') ====="
if $headroom_enabled; then
echo "--- headroom savings ---"
headroom savings 2>&1 || echo "[warn] headroom savings failed"
fi
if $rtk_enabled; then
elif $have_rtk; then
echo "--- rtk gain ---"
rtk gain 2>&1 || echo "[warn] rtk gain failed"
fi
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 ====="
echo "===== end gains report ====="

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
if ! command -v claude >/dev/null 2>&1; then
echo "Claude Code is not installed or is not on PATH." >&2
exit 127
fi
if ! command -v headroom >/dev/null 2>&1; then
echo "Headroom is unavailable; start Claude Code directly with claude-direct." >&2
exit 127
fi
if ! curl -fsS --max-time 3 "http://127.0.0.1:8787/readyz" >/dev/null; then
echo "The supervised Headroom proxy is not ready on 127.0.0.1:8787. Ensure install_headroom is enabled and check the add-on log." >&2
exit 1
fi
# Reuse the s6-supervised proxy instead of starting a competing proxy. RTK is already managed
# through the persistent Claude Code PreToolUse hook, so Headroom must not reinstall it.
exec headroom wrap claude --port 8787 --no-proxy --no-rtk -- "$@"

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
if [ -z "${HOME:-}" ]; then
echo "Claude terminal: HOME is not initialized." >&2
exit 1
fi
export SHELL="/bin/bash"
export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
workspace="${CLAUDE_TERMINAL_WORKSPACE:-${HOME}/workspace}"
session_name="${CLAUDE_TMUX_SESSION:-claude}"
if [ ! -d "$workspace" ]; then
echo "Claude terminal: workspace does not exist: $workspace" >&2
exit 1
fi
cd -- "$workspace"
# Reattach every browser connection to the same terminal session. Closing the browser detaches
# the client but leaves Claude Code and other commands running inside tmux.
exec tmux new-session -A -s "$session_name" -c "$workspace"

View File

@@ -1,178 +0,0 @@
#!/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")
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
# Capture before looping — see the matching comment in 82-claude_tools.sh: feeding the
# loop straight from `< <(bashio::config ...)` yields an empty list under errexit.
TOKENSAVE_PROJECT_PATHS="$(bashio::config 'tokensave_project_paths')"
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
continue
fi
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 <<< "$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

@@ -1,236 +0,0 @@
#!/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,218 +0,0 @@
#!/lsiopy/bin/python3
"""Claude Code PostToolUse hook: auto-compress large tool outputs through Headroom.
Registered in ~/.claude/settings.json by 82-claude_tools.sh (managed entry, matcher
"Bash|Grep|Glob|WebFetch"). Desktop-spawned Claude Code sessions cannot be routed
through the Headroom proxy (the Electron app pins ANTHROPIC_BASE_URL to the
production endpoint, headroom #869), so compression there used to depend on the
model voluntarily calling the headroom MCP tools. This hook makes it automatic for
every session type: when a matched tool returns a large output, the hook compresses
it with Headroom's rule-based pipeline and replaces the tool output via
hookSpecificOutput.updatedToolOutput, appending a retrieval marker. The original is
stored in Headroom's shared CCR store (SQLite at ~/.headroom/ccr_store.db — the
same store the headroom MCP server reads), so the model can always get the full
output back with mcp__headroom__headroom_retrieve.
Design constraints:
- Fail open: any error or non-compressible payload exits 0 with no output, leaving
the tool result untouched. A hook crash must never break a session.
- Fast path first: the payload is inspected before importing headroom (~0.6 s);
small outputs never pay the import cost.
- ML text compression (Kompress) is disabled: its model loads in the background,
which never completes inside a short-lived hook process. The rule-based
transforms (SmartCrusher for JSON, search/log/diff/tabular compressors) carry
the savings on tool output anyway; plain prose passes through unchanged.
- stderr fields are never compressed — error text must reach the model verbatim
(matching Headroom's own error-protection policy).
- File-list arrays (Glob's `filenames`, Grep's `filenames` in files_with_matches
mode — both typed `string[]` by the CLI's own output schema) are handled
separately from prose/JSON-blob fields: Headroom's SmartCrusher subsamples
JSON arrays for informational dumps, which is fine for e.g. a list of sensor
states but silently drops most paths from a file listing the model needs to
act on. Those fields are truncated deterministically instead (keep the first
N entries, append one marker string) so the model always sees a labeled cut
point rather than a shorter list it might mistake for the complete result.
"""
import json
import os
import sys
def _int_env(name: str, default: str) -> int:
try:
return int(os.environ.get(name, default))
except (TypeError, ValueError):
return int(default)
MIN_CHARS = _int_env("HEADROOM_HOOK_MIN_CHARS", "4000")
MIN_SAVED_TOKENS = _int_env("HEADROOM_HOOK_MIN_SAVED_TOKENS", "50")
ARRAY_KEEP = _int_env("HEADROOM_HOOK_ARRAY_KEEP", "40")
TTL_SECONDS = 3600 # matches the headroom MCP server's session TTL
SKIP_KEYS = {"stderr"}
def self_test() -> int:
"""Exit 0 when the interpreter can import headroom (used at registration time)."""
try:
import headroom # noqa: F401
return 0
except Exception:
return 1
def main() -> int:
if os.environ.get("HEADROOM_HOOK_DISABLE"):
return 0
try:
payload = json.load(sys.stdin)
except Exception:
return 0
if not isinstance(payload, dict):
return 0
response = payload.get("tool_response")
# Find big string/array fields before paying the headroom import cost.
def is_string_array(value):
return isinstance(value, list) and len(value) > ARRAY_KEEP and all(isinstance(v, str) for v in value)
if isinstance(response, str):
string_candidates = ["__whole__"] if len(response) >= MIN_CHARS else []
array_candidates = []
elif isinstance(response, dict):
string_candidates = [
key
for key, value in response.items()
if key not in SKIP_KEYS and isinstance(value, str) and len(value) >= MIN_CHARS
]
array_candidates = [
key for key, value in response.items() if key not in SKIP_KEYS and is_string_array(value)
]
else:
string_candidates = []
array_candidates = []
if not string_candidates and not array_candidates:
return 0
# Keep Kompress's cache probe away from the tmpfs-backed ~/.cache default.
os.environ.setdefault("HF_HOME", os.path.expanduser("~/.headroom/hf"))
from headroom import savings_ledger
from headroom.cache.compression_store import get_compression_store
from headroom.compress import compress
store = None
totals = [0, 0] # tokens before, tokens after (only for rewritten fields)
def shrink(text):
nonlocal store
result = compress(
[{"role": "tool", "content": text}],
protect_recent=0,
kompress_model="disabled",
)
compressed = result.messages[0].get("content")
if not isinstance(compressed, str):
compressed = json.dumps(compressed)
saved = result.tokens_before - result.tokens_after
if saved < MIN_SAVED_TOKENS:
return None
if store is None:
store = get_compression_store()
hash_key = store.store(
original=text,
compressed=compressed,
original_tokens=result.tokens_before,
compressed_tokens=result.tokens_after,
compression_strategy="posttooluse_hook",
ttl=TTL_SECONDS,
)
totals[0] += result.tokens_before
totals[1] += result.tokens_after
return (
f"{compressed}\n"
f"[headroom: output compressed {result.tokens_before}->{result.tokens_after} tokens; "
f"call mcp__headroom__headroom_retrieve with hash={hash_key} if you need the full original]"
)
def shrink_array(items):
nonlocal store
original_json = json.dumps(items)
if len(original_json) < MIN_CHARS:
return None
kept = items[:ARRAY_KEEP]
truncated_json = json.dumps(kept)
# No ML/token-counter call needed for a plain truncation decision; a char/4
# estimate is the same fallback Headroom's own cost estimator uses and is
# only used here to decide eligibility and annotate the marker.
tokens_before = max(1, len(original_json) // 4)
tokens_after = max(1, len(truncated_json) // 4)
if tokens_before - tokens_after < MIN_SAVED_TOKENS:
return None
if store is None:
store = get_compression_store()
hash_key = store.store(
original=original_json,
compressed=truncated_json,
original_tokens=tokens_before,
compressed_tokens=tokens_after,
compression_strategy="posttooluse_hook_array_truncate",
ttl=TTL_SECONDS,
)
totals[0] += tokens_before
totals[1] += tokens_after
remaining = len(items) - len(kept)
marker = (
f"[headroom: {remaining} more of {len(items)} entries omitted "
f"({tokens_before}->{tokens_after} tokens); call mcp__headroom__headroom_retrieve "
f"with hash={hash_key} for the complete list]"
)
return kept + [marker]
updated = None
if isinstance(response, str):
updated = shrink(response)
else:
rewritten = dict(response)
changed = False
for key in string_candidates:
new_value = shrink(rewritten[key])
if new_value is not None:
rewritten[key] = new_value
changed = True
for key in array_candidates:
new_value = shrink_array(rewritten[key])
if new_value is not None:
rewritten[key] = new_value
changed = True
if changed:
updated = rewritten
if updated is None:
return 0
savings_ledger.record_savings_event(
tokens_before=totals[0],
tokens_after=totals[1],
client="posttooluse-hook",
source="hook",
)
json.dump(
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"updatedToolOutput": updated,
}
},
sys.stdout,
)
return 0
if __name__ == "__main__":
if "--self-test" in sys.argv:
sys.exit(self_test())
try:
sys.exit(main())
except Exception:
sys.exit(0)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -3,7 +3,5 @@
"upstream_repo": "linuxserver/docker-baseimage-selkies",
"github_fulltag": true,
"slug": "claude_desktop",
"paused": false,
"upstream_version": "ubunturesolute-version-6dc44b0e",
"last_update": "2026-07-13"
"paused": false
}

View File

@@ -1,7 +1,4 @@
## 2.9.16 (2026-07-11)
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)
## 2.9.14 (2026-06-20)
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)

View File

@@ -11,7 +11,7 @@
#=== Home Assistant Addon ===#
# ARGs used in FROM must be declared before any FROM instruction
ARG BUILD_UPSTREAM="2.9.16"
ARG BUILD_UPSTREAM="2.9.14"
#################
# 1 Build Image #

View File

@@ -91,5 +91,5 @@ schema:
TZ: str?
slug: cleanuparr
url: https://github.com/alexbelgium/hassio-addons/tree/master/cleanuparr
version: "2.9.16"
version: "2.9.14"
webui: "[PROTO:ssl]://[HOST]:[PORT:11011]"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,8 +1,8 @@
{
"last_update": "2026-07-11",
"last_update": "2026-06-20",
"repository": "alexbelgium/hassio-addons",
"slug": "cleanuparr",
"source": "github",
"upstream_repo": "Cleanuparr/Cleanuparr",
"upstream_version": "2.9.16"
"upstream_version": "2.9.14"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,10 +1,4 @@
## 2.2.0 (2026-07-16)
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
## 2.1.2 (2026-07-11)
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
## 2.1.0 (2026-07-04)
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)

View File

@@ -101,4 +101,4 @@ schema:
slug: codex
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "2.2.0"
version: "2.1.0"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Some files were not shown because too many files have changed in this diff Show More