Compare commits
251 Commits
agent/remo
...
fix/claude
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fc84fa6da | ||
|
|
0ee96086c3 | ||
|
|
462767d34e | ||
|
|
32765daf99 | ||
|
|
3d2f3aa193 | ||
|
|
26101a1104 | ||
|
|
a4764364da | ||
|
|
9bbd72e70a | ||
|
|
e345752d00 | ||
|
|
7e2c8eda5e | ||
|
|
65fbc7dae2 | ||
|
|
95641d253c | ||
|
|
bdc6231aa3 | ||
|
|
ae2c29b977 | ||
|
|
3114a6cc94 | ||
|
|
297102e908 | ||
|
|
39efd5602f | ||
|
|
8b3db6e325 | ||
|
|
e70b8db0fa | ||
|
|
4872bd89c9 | ||
|
|
b3c27024d8 | ||
|
|
b8c7cc3815 | ||
|
|
3354af026f | ||
|
|
3bff6b65de | ||
|
|
efc659f452 | ||
|
|
2f245c77e1 | ||
|
|
2347cb9eae | ||
|
|
0cfe28a405 | ||
|
|
26c26e3668 | ||
|
|
64c7f2dd5b | ||
|
|
259517289a | ||
|
|
b26da2e161 | ||
|
|
9ec6c44f03 | ||
|
|
ff43c4eeba | ||
|
|
a1e68ee807 | ||
|
|
2a1412d957 | ||
|
|
b328ae242f | ||
|
|
9c0521da49 | ||
|
|
7d2c6eb9b2 | ||
|
|
f990177df8 | ||
|
|
005275315a | ||
|
|
0223cc3511 | ||
|
|
e5983f4718 | ||
|
|
587121cb1b | ||
|
|
29f2cfd198 | ||
|
|
3329c4b40d | ||
|
|
9471dffcca | ||
|
|
215327e439 | ||
|
|
8ecbfccdcd | ||
|
|
5a2efc4f9e | ||
|
|
ef0aecbde6 | ||
|
|
7a6ad72617 | ||
|
|
b5985f230e | ||
|
|
d48d1f4d8d | ||
|
|
6bf379bffc | ||
|
|
5d577ad954 | ||
|
|
85dab2ae6e | ||
|
|
ad50abc365 | ||
|
|
dc280a5caf | ||
|
|
c0c1df8c27 | ||
|
|
a38bc75f95 | ||
|
|
8073317150 | ||
|
|
15505a265f | ||
|
|
d1ceafebe8 | ||
|
|
c76b257f5d | ||
|
|
321f2fde73 | ||
|
|
45f88307d4 | ||
|
|
7cd82b2758 | ||
|
|
66d3886b80 | ||
|
|
bdd56b8057 | ||
|
|
9c55193c38 | ||
|
|
6b6233e1fa | ||
|
|
22951ac4f5 | ||
|
|
006f3052ac | ||
|
|
0ff2e8f783 | ||
|
|
3c8a32199e | ||
|
|
c8d706c9de | ||
|
|
c04f3e288c | ||
|
|
f6c2bef0be | ||
|
|
f8c447ed19 | ||
|
|
bd82e72ffa | ||
|
|
a3ecb38fea | ||
|
|
eada7a0ba0 | ||
|
|
e7921b822e | ||
|
|
aa1486c1d8 | ||
|
|
b4008c4db9 | ||
|
|
fa2328f741 | ||
|
|
e8bb55682b | ||
|
|
7fdf95940b | ||
|
|
583c5e655a | ||
|
|
b56be1f57d | ||
|
|
50150a4775 | ||
|
|
6814088369 | ||
|
|
2951b04f57 | ||
|
|
245c52fcf2 | ||
|
|
2b6c9eace7 | ||
|
|
fd268d0e6d | ||
|
|
5792d84336 | ||
|
|
bf97abdb4f | ||
|
|
7de4789196 | ||
|
|
34e8a75bf8 | ||
|
|
652be8b13c | ||
|
|
78f5289518 | ||
|
|
12384ee606 | ||
|
|
df0dacf6b9 | ||
|
|
2cbf2a6f08 | ||
|
|
adcd892e62 | ||
|
|
980be49d9b | ||
|
|
9cff4f83b9 | ||
|
|
688d1cbdcf | ||
|
|
02bbfa86c3 | ||
|
|
a5916d9236 | ||
|
|
371c206fc0 | ||
|
|
75ce8e94e4 | ||
|
|
0b7b0ac1df | ||
|
|
2f4ab956db | ||
|
|
08a7bd35cb | ||
|
|
a313475d92 | ||
|
|
087d23eeaf | ||
|
|
a9a6b22a62 | ||
|
|
b16305a9e6 | ||
|
|
cc427f0c20 | ||
|
|
68120a84da | ||
|
|
776d063161 | ||
|
|
34135ba471 | ||
|
|
ab02e92ad1 | ||
|
|
6f93187535 | ||
|
|
a90fe9ee4a | ||
|
|
6bb4cb735d | ||
|
|
299c97c83f | ||
|
|
ad5eba5657 | ||
|
|
bd6daa32b7 | ||
|
|
7df08e4f84 | ||
|
|
b8421be1e9 | ||
|
|
30f99d738b | ||
|
|
d44b38981e | ||
|
|
c6c5197eb8 | ||
|
|
a966369e96 | ||
|
|
0363265d84 | ||
|
|
bb2146866f | ||
|
|
21b9783fe2 | ||
|
|
8162d42e41 | ||
|
|
4b848c9054 | ||
|
|
731f099c8c | ||
|
|
e56dcf7abd | ||
|
|
e25b4ec1d3 | ||
|
|
9161c28e27 | ||
|
|
82af311c37 | ||
|
|
85dd1669fa | ||
|
|
98962ac296 | ||
|
|
6d199b146b | ||
|
|
b0750a8a7f | ||
|
|
ef9f76e91c | ||
|
|
8bc048b993 | ||
|
|
26533b6697 | ||
|
|
3eafa3e028 | ||
|
|
9173829335 | ||
|
|
6b50abe96d | ||
|
|
9a4f384d5a | ||
|
|
521d555615 | ||
|
|
a5bba509ef | ||
|
|
246a7ce157 | ||
|
|
44f576699d | ||
|
|
afe45b7a75 | ||
|
|
796a4132bf | ||
|
|
e17cc922f9 | ||
|
|
cedccabef2 | ||
|
|
c8a52ba915 | ||
|
|
2468d11400 | ||
|
|
3ad3b1d1ba | ||
|
|
3c5212b4ef | ||
|
|
289b3b53f1 | ||
|
|
19c3cf8252 | ||
|
|
dc206213ca | ||
|
|
5755df5181 | ||
|
|
0b6c410001 | ||
|
|
2a498af497 | ||
|
|
b620907a6a | ||
|
|
98629f7214 | ||
|
|
769cfc278e | ||
|
|
9d21c49fa5 | ||
|
|
49d78a32d5 | ||
|
|
5208dd3ce9 | ||
|
|
055f6e58f3 | ||
|
|
e2bec544cb | ||
|
|
23f756b4db | ||
|
|
efa5ed9b59 | ||
|
|
82c784ce6e | ||
|
|
83b791e6c3 | ||
|
|
c3a8e1d57b | ||
|
|
382de98a9b | ||
|
|
ca6c6a54d3 | ||
|
|
57d8edb445 | ||
|
|
d9252da5a3 | ||
|
|
deb51a0bed | ||
|
|
dbaa8a8a98 | ||
|
|
4cf79224f7 | ||
|
|
90fd373c2c | ||
|
|
e930e36e0d | ||
|
|
35c3077bec | ||
|
|
06e0a32f81 | ||
|
|
22f9490b77 | ||
|
|
72851eea95 | ||
|
|
2bc142b5fe | ||
|
|
3e5649f00f | ||
|
|
8c7a43b5be | ||
|
|
e7a6add76f | ||
|
|
0d56b4dded | ||
|
|
7ee213406d | ||
|
|
e6210d484d | ||
|
|
71c67e7eef | ||
|
|
2aaf940b73 | ||
|
|
842c237965 | ||
|
|
8788211f8a | ||
|
|
ea3e5e425b | ||
|
|
554d7dc04f | ||
|
|
29ca739709 | ||
|
|
e8260efe37 | ||
|
|
682a96fc8e | ||
|
|
22bbbfdf75 | ||
|
|
481094574d | ||
|
|
534a526185 | ||
|
|
b98ae69a6c | ||
|
|
af68eece53 | ||
|
|
c8f5ec8f20 | ||
|
|
aa77249314 | ||
|
|
6e02e611f4 | ||
|
|
91ed6c3f46 | ||
|
|
b21ce72e0e | ||
|
|
839cfd1382 | ||
|
|
ae22240269 | ||
|
|
91429643ef | ||
|
|
507ea9fb8c | ||
|
|
434aa76c2a | ||
|
|
397a6eed88 | ||
|
|
859e47772a | ||
|
|
8153a944d8 | ||
|
|
6f143deb49 | ||
|
|
0fc95bfe88 | ||
|
|
c53263a29d | ||
|
|
0c651312ab | ||
|
|
6b4c11cbd8 | ||
|
|
424573548e | ||
|
|
effd7f4319 | ||
|
|
69458eb13d | ||
|
|
2bbc48f7ab | ||
|
|
94135b9b05 | ||
|
|
7b3eb17efd | ||
|
|
5d7d6ff334 | ||
|
|
bd9f4f3d23 | ||
|
|
918b9477b0 |
10
.github/stargazer_countries.csv
vendored
@@ -162,6 +162,7 @@ DARKNAGAN,France
|
||||
DDanii,
|
||||
DMurzNN,
|
||||
DUC750,
|
||||
DY-hub,
|
||||
DaFlowah,
|
||||
DaJonas94,
|
||||
Daafip,Netherlands
|
||||
@@ -238,6 +239,7 @@ EtienneMD,
|
||||
Evel270,
|
||||
Everestlion,
|
||||
EvertJob,
|
||||
Exlatis,
|
||||
Extrunder,
|
||||
F0264,
|
||||
F4bsi,Germany
|
||||
@@ -563,6 +565,7 @@ PhoenixTwoFive,Germany
|
||||
PhysShell,
|
||||
PierreNa,France
|
||||
PietroSpina,
|
||||
Pingmin,
|
||||
PiotrKrzyzek,United States
|
||||
PiratesGhost,
|
||||
Pixelzeus,
|
||||
@@ -897,10 +900,12 @@ antorimba,
|
||||
antx-code,
|
||||
anyezhe,
|
||||
aorosora,
|
||||
araminimichael,
|
||||
arbal,United States
|
||||
ardemk,
|
||||
ared469,
|
||||
arethefreshest,
|
||||
arozoire,
|
||||
arpit-mehra,
|
||||
artemave,France
|
||||
artemdanielov,
|
||||
@@ -1156,6 +1161,7 @@ danbruno,
|
||||
danctrl,Germany
|
||||
danez,United States
|
||||
danieldotnl,Netherlands
|
||||
danishru,
|
||||
dannybeeckman,
|
||||
dannybloomfield,United States
|
||||
danveitch76,
|
||||
@@ -1744,6 +1750,7 @@ ljsquare,
|
||||
llabourdeth,
|
||||
llewy,
|
||||
llfjahn,
|
||||
llugo,
|
||||
lmalmoreno,Brazil
|
||||
lnrdmx,
|
||||
loc4t3llix,
|
||||
@@ -2050,6 +2057,7 @@ pedrolicassali,
|
||||
pedromfa,
|
||||
pedrware,Portugal
|
||||
peeetek,
|
||||
peggleg,
|
||||
pejannl,
|
||||
pem884,United States
|
||||
pepelatc,
|
||||
@@ -2273,6 +2281,7 @@ skalingclouds,United States
|
||||
skamaleo,
|
||||
skavieller,
|
||||
skipper00,
|
||||
skoducks,United States
|
||||
skylidefr,
|
||||
skynet-network,
|
||||
slimehands,
|
||||
@@ -2532,6 +2541,7 @@ williamcorsel,Netherlands
|
||||
willigenburggihaux,
|
||||
willnewcombe,United Kingdom
|
||||
wimb0,
|
||||
wingerasc,
|
||||
witold-gren,Poland
|
||||
wonderfulhuber,
|
||||
wonkygecko,United States
|
||||
|
||||
|
BIN
.github/stargazer_map.png
vendored
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 60 KiB |
BIN
.github/stats.png
vendored
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
.github/stats_addons.png
vendored
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 4.4 KiB |
2
.github/workflows/onpr_check-pr.yaml
vendored
@@ -35,6 +35,7 @@ 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)
|
||||
@@ -208,6 +209,7 @@ jobs:
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: ${{ matrix.addon }}
|
||||
platforms: linux/arm64
|
||||
push: false
|
||||
load: true
|
||||
file: ${{ matrix.addon }}/Dockerfile
|
||||
|
||||
3
.github/workflows/onpush_builder.yaml
vendored
@@ -300,7 +300,7 @@ jobs:
|
||||
|
||||
- name: Build ${{ matrix.addon }} add-on
|
||||
if: steps.info.outputs.build_arch == 'true' && steps.info.outputs.has_dockerfile == 'true'
|
||||
uses: home-assistant/builder/actions/build-image@2026.03.2
|
||||
uses: home-assistant/builder/actions/build-image@2026.06.0
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
cache-gha: "false"
|
||||
@@ -433,4 +433,3 @@ jobs:
|
||||
done
|
||||
|
||||
git push origin HEAD:master
|
||||
|
||||
|
||||
38
README.md
@@ -56,19 +56,19 @@ If you want to do add the repository manually, please follow the procedure highl
|
||||
|
||||
### Number of addons
|
||||
|
||||
- In the repository : 135
|
||||
- Installed : 508490
|
||||
- In the repository : 136
|
||||
- Installed : 626324
|
||||
|
||||
### Top 3
|
||||
|
||||
1. Arpspoof (59486x)
|
||||
2. Sponsorblockcast (55582x)
|
||||
3. Flaresolverr (44486x)
|
||||
1. Arpspoof (86665x)
|
||||
2. Sponsorblockcast (82801x)
|
||||
3. Jellyfin (71957x)
|
||||
|
||||
### Architectures used
|
||||
|
||||
- amd64: 88%
|
||||
- aarch64: 12%
|
||||
- amd64: 90%
|
||||
- aarch64: 10%
|
||||
|
||||
### 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 (59486x)](arpspoof/) : block internet connection for local network devices
|
||||
✓ [Arpspoof (86665x)](arpspoof/) : block internet connection for local network devices
|
||||
|
||||
  
|
||||

|
||||
@@ -143,6 +143,17 @@ If you want to do add the repository manually, please follow the procedure highl
|
||||
![amd64][amd64-badge]
|
||||
![ingress][ingress-badge]
|
||||
|
||||
✓  [BirdNET-Pi (zach7036)](birdnet-pi-zach/) : Realtime acoustic bird classification system
|
||||
|
||||
  
|
||||

|
||||
![aarch64][aarch64-badge]
|
||||
![amd64][amd64-badge]
|
||||
![ingress][ingress-badge]
|
||||
![mqtt][mqtt-badge]
|
||||
![smb][smb-badge]
|
||||
![localdisks][localdisks-badge]
|
||||
|
||||
✓  [BirdNET-PiPy](birdnet-pipy/) : BirdNET-PiPy bird detection with a modern web dashboard
|
||||
|
||||
  
|
||||
@@ -247,13 +258,15 @@ If you want to do add the repository manually, please follow the procedure highl
|
||||
![amd64][amd64-badge]
|
||||
![ingress][ingress-badge]
|
||||
|
||||
✓  [Claude Desktop](claude_desktop/) : Claude Desktop streamed through a browser with LinuxServer Selkies
|
||||
✓  [Claude Desktop](claude_desktop/) : Claude Desktop with Headroom, RTK, and TokenSave optimization
|
||||
|
||||
  
|
||||

|
||||
![aarch64][aarch64-badge]
|
||||
![amd64][amd64-badge]
|
||||
![ingress][ingress-badge]
|
||||
![smb][smb-badge]
|
||||
![localdisks][localdisks-badge]
|
||||
|
||||
✓  [Cleanuparr](cleanuparr/) : Automatically removes stuck and unwanted downloads from your *arr and download clients
|
||||
|
||||
@@ -302,6 +315,7 @@ If you want to do add the repository manually, please follow the procedure highl
|
||||
✓ [Elasticsearch server](elasticsearch/) : Free and Open, Distributed, RESTful Search Engine
|
||||
|
||||
  
|
||||

|
||||
![aarch64][aarch64-badge]
|
||||
![amd64][amd64-badge]
|
||||
|
||||
@@ -508,7 +522,7 @@ If you want to do add the repository manually, please follow the procedure highl
|
||||
![smb][smb-badge]
|
||||
![localdisks][localdisks-badge]
|
||||
|
||||
✓  [Jellyfin NAS](jellyfin/) : A free Software Media System that puts you in control of managing and streaming your media
|
||||
✓  [Jellyfin (71957x) NAS](jellyfin/) : A free Software Media System that puts you in control of managing and streaming your media
|
||||
|
||||
  
|
||||

|
||||
@@ -571,7 +585,7 @@ If you want to do add the repository manually, please follow the procedure highl
|
||||
![aarch64][aarch64-badge]
|
||||
![amd64][amd64-badge]
|
||||
|
||||
✓  [Maintainerr](maintainerr/) : Rule-based media cleanup tool for Plex, Jellyfin and Emby. Creates collections and optionally deletes unwatched content.
|
||||
✓  [Maintainerr](maintainerr/) : Rule-based media cleanup tool for Plex, Jellyfin (71957x) and Emby. Creates collections and optionally deletes unwatched content.
|
||||
|
||||
  
|
||||

|
||||
@@ -885,7 +899,7 @@ If you want to do add the repository manually, please follow the procedure highl
|
||||
![smb][smb-badge]
|
||||
![localdisks][localdisks-badge]
|
||||
|
||||
✓  [Seerr](seerr/) : Open-source media request and discovery manager for Jellyfin, Plex, and Emby
|
||||
✓  [Seerr](seerr/) : Open-source media request and discovery manager for Jellyfin (71957x), Plex, and Emby
|
||||
|
||||
  
|
||||

|
||||
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,4 +1,7 @@
|
||||
|
||||
## 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)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Aurral
|
||||
version: "1.76.51"
|
||||
version: "1.76.52"
|
||||
slug: aurral
|
||||
description: >-
|
||||
Self-hosted music discovery, request management, flows, and playlist
|
||||
|
||||
BIN
aurral/stats.png
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"last_update": "2026-06-17",
|
||||
"last_update": "2026-07-11",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "aurral",
|
||||
"source": "github",
|
||||
"upstream_repo": "lklynet/aurral",
|
||||
"upstream_version": "1.76.51",
|
||||
"upstream_version": "1.76.52",
|
||||
"github_beta": false
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
baikal/stats.png
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 1.7 KiB |
BIN
bazarr/stats.png
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,3 +1,7 @@
|
||||
## 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)
|
||||
|
||||
@@ -127,5 +127,5 @@ slug: birdnet-go-dev
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
usb: true
|
||||
version: "source-20260709"
|
||||
version: "source-20260716"
|
||||
video: true
|
||||
|
||||
@@ -77,10 +77,27 @@ 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
|
||||
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
|
||||
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
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1,3 +1,6 @@
|
||||
|
||||
## 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)
|
||||
|
||||
@@ -128,4 +128,4 @@ slug: birdnet-go
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-go
|
||||
usb: true
|
||||
version: "nightly-20260615-4"
|
||||
version: "20260712"
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -2,10 +2,10 @@
|
||||
"github_beta": true,
|
||||
"github_exclude": "-4",
|
||||
"github_fulltag": true,
|
||||
"last_update": "2026-06-17",
|
||||
"last_update": "2026-07-13",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "birdnet-go",
|
||||
"source": "github",
|
||||
"upstream_repo": "tphakala/birdnet-go",
|
||||
"upstream_version": "nightly-20260615"
|
||||
"upstream_version": "20260712"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
## 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)
|
||||
|
||||
@@ -74,17 +74,6 @@ 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
|
||||
@@ -110,6 +99,16 @@ 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 && \
|
||||
\
|
||||
|
||||
@@ -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
|
||||
version: 2026.07.10-1
|
||||
video: true
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
# 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
|
||||
|
||||
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -1,4 +1,10 @@
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
@@ -96,4 +96,4 @@ schema:
|
||||
ssl: bool?
|
||||
slug: birdnet-pipy
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-pipy
|
||||
version: "0.8.2.1"
|
||||
version: "0.8.4"
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"last_update": "2026-07-04",
|
||||
"last_update": "2026-07-13",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "birdnet-pipy",
|
||||
"source": "github",
|
||||
"upstream_repo": "Suncuss/BirdNET-PiPy",
|
||||
"upstream_version": "0.8.2"
|
||||
"upstream_version": "0.8.4"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,4 +1,7 @@
|
||||
|
||||
## 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)
|
||||
|
||||
|
||||
@@ -69,5 +69,5 @@ slug: brave
|
||||
tmpfs: true
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "1.92.134-ls109"
|
||||
version: "1.92.139-ls112"
|
||||
video: true
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"github_fulltag": "true",
|
||||
"last_update": "2026-07-04",
|
||||
"last_update": "2026-07-11",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "brave",
|
||||
"source": "github",
|
||||
"upstream_repo": "linuxserver/docker-brave",
|
||||
"upstream_version": "1.92.134-ls109"
|
||||
"upstream_version": "1.92.139-ls112"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
|
||||
## 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)
|
||||
|
||||
|
||||
@@ -71,5 +71,5 @@ slug: chromium
|
||||
tmpfs: true
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "version-7148c2a3"
|
||||
version: "version-30a7c401"
|
||||
video: true
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"github_fulltag": "true",
|
||||
"last_update": "2026-07-04",
|
||||
"last_update": "2026-07-11",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "chromium",
|
||||
"source": "github",
|
||||
"upstream_repo": "linuxserver/docker-chromium",
|
||||
"upstream_version": "version-7148c2a3"
|
||||
"upstream_version": "version-30a7c401"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,3 +1,93 @@
|
||||
## 1.31 (16-07-2026)
|
||||
|
||||
- Fix Home Assistant ingress remaining on `Waiting for stream`: current Selkies WebSocket mode connects through `/api/websockets`, while the add-on inherited an older nginx template that proxied only `/websocket`. Replace the template rewrite with an explicit ingress server that proxies `/api/` to Selkies on port 8082, retains `/websocket` compatibility, and declares `ingress_port: 3001` so nginx always receives a valid listen port.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -9,7 +9,32 @@
|
||||
|
||||
ARG BUILD_FROM
|
||||
ARG BUILD_VERSION
|
||||
ARG RTK_VERSION="v0.43.0"
|
||||
ARG RTK_COMMIT="5a7880d404db8364d602f2ecdc41dd790f64013f"
|
||||
ARG TOKENSAVE_VERSION="7.2.0"
|
||||
|
||||
# The upstream aarch64 release is cross-built on ubuntu-latest and requires
|
||||
# GLIBC 2.39. Build the pinned source on Bookworm instead so it is compatible
|
||||
# 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 #
|
||||
@@ -45,17 +70,21 @@ RUN curl -fsSL --retry 3 --retry-delay 2 \
|
||||
# 3 Install apps #
|
||||
##################
|
||||
|
||||
# Add rootfs
|
||||
# Add rootfs. Only the directories this add-on ships scripts in are traversed, so the chmod
|
||||
# cannot alter executables elsewhere in the image.
|
||||
COPY rootfs/ /
|
||||
RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \; && \
|
||||
chmod +x /usr/local/bin/claude-direct /usr/local/bin/claude-headroom /usr/local/bin/claude-terminal-shell
|
||||
RUN find /etc/cont-init.d /etc/s6-overlay /defaults /usr/local/bin -type f \
|
||||
\( -name "*.sh" -o -name "run" -o -name "finish" -o -name "ha-cli" \) -print -exec chmod +x {} \; && \
|
||||
chmod +x /usr/local/bin/claude
|
||||
|
||||
# Uses /bin for compatibility purposes
|
||||
# hadolint ignore=DL4005
|
||||
RUN if [ ! -f /bin/sh ] && [ -f /usr/bin/sh ]; then ln -s /usr/bin/sh /bin/sh; fi && \
|
||||
if [ ! -f /bin/bash ] && [ -f /usr/bin/bash ]; then ln -s /usr/bin/bash /bin/bash; fi
|
||||
|
||||
# Install Claude Desktop, Claude Code, browser-terminal tooling, and Python tooling
|
||||
# Install Claude Desktop, Claude Code, Python tooling, and lightweight local validators.
|
||||
# gnome-keyring provides the Secret Service backend Electron safeStorage needs to persist
|
||||
# sign-in and dispatch grants.
|
||||
RUN install -d -m 0755 /etc/apt/keyrings && \
|
||||
curl -fsSLo /usr/share/keyrings/claude-desktop-archive-keyring.asc https://downloads.claude.ai/claude-desktop/key.asc && \
|
||||
curl -fsSLo /etc/apt/keyrings/claude-code.asc https://downloads.claude.ai/keys/claude-code.asc && \
|
||||
@@ -71,33 +100,51 @@ RUN install -d -m 0755 /etc/apt/keyrings && \
|
||||
git \
|
||||
gh \
|
||||
ripgrep \
|
||||
tmux && \
|
||||
jq \
|
||||
shellcheck \
|
||||
yamllint && \
|
||||
test -x /usr/bin/claude && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ttyd is not packaged in Debian bookworm. Install the pinned upstream static binary.
|
||||
ARG TTYD_VERSION="1.7.7"
|
||||
# Install the current upstream hadolint and actionlint releases for both supported
|
||||
ARG HADOLINT_VERSION=v2.14.0
|
||||
ARG ACTIONLINT_VERSION=v1.7.12
|
||||
|
||||
RUN set -eux; \
|
||||
case "$(dpkg --print-architecture)" in \
|
||||
amd64) ttyd_arch="x86_64" ;; \
|
||||
arm64) ttyd_arch="aarch64" ;; \
|
||||
*) echo "Unsupported architecture for ttyd: $(dpkg --print-architecture)" >&2; exit 1 ;; \
|
||||
case "${BUILD_ARCH}" in \
|
||||
amd64) hadolint_arch="x86_64"; actionlint_arch="amd64" ;; \
|
||||
aarch64) hadolint_arch="arm64"; actionlint_arch="arm64" ;; \
|
||||
*) echo "Unsupported validation-tools architecture: ${BUILD_ARCH}" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
curl -fsSL --retry 3 --retry-delay 2 \
|
||||
-o /usr/local/bin/ttyd \
|
||||
"https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.${ttyd_arch}"; \
|
||||
chmod 0755 /usr/local/bin/ttyd; \
|
||||
/usr/local/bin/ttyd --version
|
||||
-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
|
||||
|
||||
# Install only the Headroom proxy, code-compression, and MCP features used by this add-on.
|
||||
# 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.
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
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 && \
|
||||
pip3 install --break-system-packages "headroom-ai[proxy,code,mcp]" mcp-proxy uv websockets && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /root/.cache
|
||||
|
||||
@@ -145,7 +192,6 @@ CMD [ "/ha_entrypoint.sh" ]
|
||||
# 5 Labels #
|
||||
############
|
||||
|
||||
ARG BUILD_ARCH
|
||||
ARG BUILD_DATE
|
||||
ARG BUILD_DESCRIPTION
|
||||
ARG BUILD_NAME
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
![Supports amd64 Architecture][amd64-shield]
|
||||
![Project Maintenance][maintenance-shield]
|
||||
|
||||
Run Claude Desktop and an optional persistent Claude Code web terminal in one
|
||||
LinuxServer.io Selkies add-on.
|
||||
Run Claude Desktop in a LinuxServer.io Selkies add-on, with Headroom context
|
||||
compression, RTK Bash-output acceleration, and TokenSave semantic code
|
||||
intelligence wired in by default.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -20,151 +21,252 @@ currently does not include Computer Use or dictation.
|
||||
|
||||
## Architecture
|
||||
|
||||
Claude Desktop and Claude Code run as separate clients inside the same add-on.
|
||||
They share the configured persistent home directory, Git credentials,
|
||||
repositories, Claude Code configuration, Headroom storage, and RTK
|
||||
configuration, but they do not share or hand off a conversation.
|
||||
Everything is built around the Claude Desktop app. Claude Code is installed in
|
||||
the same image but is not exposed as a standalone service: Claude Desktop's
|
||||
cowork and dispatch sessions run it internally, and they pick up the shared
|
||||
Claude Code configuration (`~/.claude`), hooks, MCP servers, permissions, and
|
||||
PATH tools.
|
||||
|
||||
- **Claude Desktop** uses Headroom through its MCP tools.
|
||||
- **Claude Code** uses Headroom's supported `headroom wrap claude` integration.
|
||||
- **RTK** filters Claude Code Bash output through its `PreToolUse` hook.
|
||||
- **tmux** keeps the terminal session running when the browser disconnects.
|
||||
- **Claude Code sessions inside Desktop** get the same MCP servers, 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.
|
||||
|
||||
## Features
|
||||
|
||||
- Claude Desktop in single-app Selkies mode.
|
||||
- Home Assistant ingress support for Claude Desktop.
|
||||
- Official Claude Code stable package installed in the same image.
|
||||
- Optional authenticated `ttyd` web terminal on port `7681`.
|
||||
- Persistent `tmux` session shared by reconnecting terminal clients.
|
||||
- Claude Desktop in single-app Selkies mode with Home Assistant ingress.
|
||||
- Official Claude Code stable package powering Desktop cowork/dispatch
|
||||
sessions.
|
||||
- Persistent `$HOME` at the configured `data_location` (default `/data/data`),
|
||||
preserving Desktop and Claude Code state across restarts.
|
||||
- Persistent sign-in through a bundled, auto-unlocked gnome-keyring.
|
||||
- Configurable Claude Code permissions: strict prompts, automatic safe-action
|
||||
approval, or explicit full bypass for trusted installations.
|
||||
- Automatic non-root runtime enforcement for bypass mode, including root-console
|
||||
wrapper launches.
|
||||
- Optional runtime Claude Desktop updates from Anthropic's apt repository.
|
||||
- Optional extra apt and pip package installation.
|
||||
- Baked-in `git`, GitHub CLI (`gh`), `ripgrep`, and terminal tooling.
|
||||
- Optional extra apt and pip package installation (pip installs use `uv`).
|
||||
- Baked-in `git`, GitHub CLI (`gh`), `ripgrep`, `jq`, `shellcheck`, `yamllint`,
|
||||
`hadolint`, and `actionlint`.
|
||||
- Custom script support through the repository standard `claude_desktop.sh`.
|
||||
- Optional bundled Claude Code optimization tools: Headroom, RTK, and Caveman.
|
||||
- Headroom dashboard exposed on mapped port `8787` when enabled.
|
||||
- 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.
|
||||
- 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` | `0` / `0` | Numeric user and group applied by the LinuxServer initialization. |
|
||||
| `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. |
|
||||
| `TZ` | | Optional timezone, for example `Europe/Brussels`. |
|
||||
| `KEYBOARD` | | Optional Selkies keyboard layout. |
|
||||
| `PASSWORD` | | Optional password for direct Selkies ports and compatibility fallback for terminal authentication. |
|
||||
| `PASSWORD` | | Optional password for direct Selkies ports. |
|
||||
| `DRINODE` | | Optional GPU device override for Selkies. |
|
||||
| `DNS_server` | `8.8.8.8` | DNS server used by the standard DNS module. |
|
||||
| `auto_update` | `true` | Upgrade `claude-desktop` from Anthropic's apt repository at startup. |
|
||||
| `enable_terminal` | `true` | Enable the supervised Claude Code web-terminal service. |
|
||||
| `terminal_username` | `claude` | Username used by ttyd Basic Authentication. |
|
||||
| `terminal_password` | | Dedicated terminal password. The service idles when this and `PASSWORD` are empty. |
|
||||
| `terminal_workspace` | | Initial directory; defaults to `<data_location>/workspace`. |
|
||||
| `install_headroom` | `true` | Enable Headroom MCP for Desktop and the supervised local proxy reused by `claude-headroom`. |
|
||||
| `install_rtk` | `true` | Configure RTK's Claude Code `PreToolUse` hook. |
|
||||
| `install_caveman` | `true` | Install the Caveman Claude Code plugin in the persistent Claude home. |
|
||||
| `permission_mode` | `auto` | Claude Code permission policy: `strict`, `auto`, or `bypass`. |
|
||||
| `install_headroom` | `true` | Register Headroom MCP and run the supervised local proxy. |
|
||||
| `headroom_wrap_claude_code` | `true` | Route PATH-based Claude Code launches through the already-running Headroom proxy. |
|
||||
| `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. |
|
||||
| `install_github_cli` | `true` | Enable setup checks for the baked-in `git` and `gh` commands. |
|
||||
| `github_token` | | Optional GitHub token used to authenticate `gh` and Git operations. |
|
||||
| `github_username` | | Optional global Git author name. |
|
||||
| `github_email` | | Optional global Git author email. |
|
||||
| `ha_smart_context` | `true` | Enable Home Assistant smart context support for Claude tooling. |
|
||||
| `enable_ha_mcp` | `true` | Enable Home Assistant MCP support for Claude tooling. |
|
||||
| `dangerously_skip_permissions` | `false` | Reserved compatibility option; it is not applied by the terminal launcher. |
|
||||
| `enable_ha_mcp` | `false` | Register Home Assistant's MCP server in Claude (requires `ha_mcp_token`). |
|
||||
| `ha_mcp_url` | `http://homeassistant:8123/api/mcp` | Streamable HTTP endpoint of Home Assistant's MCP Server integration. |
|
||||
| `ha_mcp_token` | | Home Assistant long-lived access token used by the MCP bridge. |
|
||||
| `enable_ha_api_helper` | `true` | Ship the `ha-cli` Core-API helper and add guidance so Claude can configure Home Assistant without a `/config` mount. |
|
||||
| `additional_apps` | | Comma-separated Debian apt packages to install at startup. |
|
||||
| `additional_pip` | | Comma-separated pip packages installed with `--break-system-packages`. |
|
||||
| `data_location` | `/data/data` | Persistent home directory for both Claude clients and tooling. |
|
||||
| `additional_pip` | | Comma-separated pip packages installed at startup (via `uv`). |
|
||||
| `data_location` | `/data/data` | Persistent home directory for Claude and tooling. |
|
||||
| `env_vars` | `[]` | Additional environment variables exported inside the container. |
|
||||
|
||||
### 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` in
|
||||
Claude Desktop and starts a supervised local Headroom backend. Desktop can use
|
||||
`headroom_compress`, `headroom_retrieve`, and `headroom_stats` through MCP.
|
||||
When `install_headroom` is enabled, the add-on registers `headroom mcp serve`
|
||||
with the explicit local proxy URL in Claude Desktop and Claude Code, then starts
|
||||
a supervised Headroom backend on `127.0.0.1:8787`.
|
||||
|
||||
Claude Desktop overrides `ANTHROPIC_BASE_URL`, so it is deliberately launched
|
||||
without proxy injection. The 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.
|
||||
Claude Desktop overrides `ANTHROPIC_BASE_URL`, so Desktop chat deliberately uses
|
||||
the MCP integration. The `/usr/local/bin/claude` wrapper routes PATH-based Claude
|
||||
Code sessions through `headroom wrap claude --no-proxy`, reusing the supervised
|
||||
backend without starting a second proxy.
|
||||
|
||||
The Headroom dashboard remains available at:
|
||||
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.
|
||||
|
||||
```text
|
||||
http://<home-assistant-host>:8787/dashboard
|
||||
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
|
||||
```
|
||||
|
||||
when the `8787/tcp` port is mapped. Treat this endpoint as sensitive and do not
|
||||
expose it directly to the public internet.
|
||||
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.
|
||||
|
||||
## Custom scripts
|
||||
|
||||
@@ -178,10 +280,14 @@ the image.
|
||||
Persistent state is stored in the configured `data_location` (default
|
||||
`/data/data`):
|
||||
|
||||
- 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
|
||||
- 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
|
||||
|
||||
Volatile cache data is redirected to `/tmp/cache` through `$XDG_CACHE_HOME` and
|
||||
`$HOME/.cache`.
|
||||
|
||||
@@ -4,7 +4,10 @@ Two related sign-in problems when Claude Desktop runs inside the LinuxServer Sel
|
||||
streamed desktop.
|
||||
|
||||
**Status:**
|
||||
- **Shipped:** Problem B (keyring persistence) is implemented in v1.4 (Dockerfile + `rootfs/defaults/autostart`).
|
||||
- **Shipped:** Problem B (keyring persistence) — the `autostart` bootstrap landed in v1.4, but
|
||||
the `gnome-keyring` package itself was missing from the image until v1.17 (the bootstrap
|
||||
silently no-oped and Electron logged "safeStorage encryption is not available"). Fixed in
|
||||
v1.17: the Dockerfile now installs `gnome-keyring`.
|
||||
- **Planned only:** Problem A (in-desktop browser for OAuth) is intentionally not implemented.
|
||||
The image ships no browser; complete the login with the user-side workaround below.
|
||||
|
||||
@@ -50,8 +53,9 @@ magic link into the in-session Chromium (not a phone).
|
||||
### User-side workaround (no rebuild)
|
||||
- Add-on Configuration → `additional_apps: chromium`, restart (installed by
|
||||
`rootfs/etc/cont-init.d/80-configuration.sh`).
|
||||
- Run the two `xdg-settings`/`xdg-mime` commands once in an in-session terminal, or add them
|
||||
to the custom script `/addon_configs/db21ed7f_claude-desktop/claude-desktop.sh`.
|
||||
- Add the two `xdg-settings`/`xdg-mime` commands to the custom script
|
||||
`/addon_configs/db21ed7f_claude-desktop/claude_desktop.sh` (the image ships no standalone
|
||||
terminal).
|
||||
|
||||
---
|
||||
|
||||
@@ -94,14 +98,14 @@ Claude Desktop uses. No extra `dbus-launch` is needed.
|
||||
then exposes the Secret Service and exports `GNOME_KEYRING_CONTROL`/`SSH_AUTH_SOCK`.
|
||||
- `--password-store=gnome-libsecret` forces Electron to use the libsecret backend instead
|
||||
of falling back to plaintext.
|
||||
3. Persistence: the keyring DB lives in `$HOME/.local/share/keyrings/` and `HOME=/config/data`
|
||||
3. Persistence: the keyring DB lives in `$HOME/.local/share/keyrings/` and `HOME=/data/data`
|
||||
(persistent add-on storage), so the empty-password login keyring survives restarts and is
|
||||
re-unlocked automatically each boot by the same `autostart` line — the sign-in then sticks.
|
||||
|
||||
### User-side workaround (no rebuild)
|
||||
- Add-on Configuration → `additional_apps: gnome-keyring, libsecret-1-0, dbus-x11`, restart.
|
||||
- Add the keyring-start lines above to the custom script
|
||||
`/addon_configs/db21ed7f_claude-desktop/claude-desktop.sh`, and relaunch Claude Desktop
|
||||
`/addon_configs/db21ed7f_claude-desktop/claude_desktop.sh`, and relaunch Claude Desktop
|
||||
with `--password-store=gnome-libsecret` (e.g. edit the in-session openbox autostart).
|
||||
|
||||
---
|
||||
|
||||
@@ -2,7 +2,7 @@ arch:
|
||||
- aarch64
|
||||
- amd64
|
||||
audio: true
|
||||
description: Claude Desktop with a persistent Claude Code web terminal in one add-on
|
||||
description: "Claude Desktop with Headroom, RTK, and TokenSave optimization"
|
||||
devices:
|
||||
- /dev/dri
|
||||
- /dev/dri/card0
|
||||
@@ -13,13 +13,14 @@ environment:
|
||||
AUTO_GPU: "1"
|
||||
FM_HOME: /data/data
|
||||
HOME: /data/data
|
||||
PGID: "0"
|
||||
PUID: "0"
|
||||
PGID: "1000"
|
||||
PUID: "1000"
|
||||
SELKIES_FRAMERATE: "30"
|
||||
START_DOCKER: "false"
|
||||
TITLE: Claude Desktop
|
||||
image: ghcr.io/alexbelgium/claude_desktop-{arch}
|
||||
ingress: true
|
||||
ingress_port: 3001
|
||||
init: false
|
||||
hassio_api: true
|
||||
hassio_role: manager
|
||||
@@ -34,36 +35,38 @@ 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: ""
|
||||
ha_smart_context: true
|
||||
enable_ha_mcp: true
|
||||
dangerously_skip_permissions: false
|
||||
enable_terminal: true
|
||||
terminal_username: claude
|
||||
terminal_password: ""
|
||||
terminal_workspace: ""
|
||||
enable_ha_mcp: false
|
||||
ha_mcp_url: http://homeassistant:8123/api/mcp
|
||||
ha_mcp_token: ""
|
||||
enable_ha_api_helper: true
|
||||
github_token: ""
|
||||
github_username: ""
|
||||
install_caveman: true
|
||||
enable_tools_health_report: true
|
||||
expose_headroom_dashboard: false
|
||||
headroom_auto_compress: true
|
||||
headroom_wrap_claude_code: true
|
||||
install_caveman: false
|
||||
install_github_cli: true
|
||||
install_headroom: true
|
||||
install_rtk: true
|
||||
install_tokensave: true
|
||||
permission_mode: auto
|
||||
tokensave_project_paths: []
|
||||
panel_admin: false
|
||||
panel_icon: mdi:robot-happy
|
||||
ports:
|
||||
3001/tcp: null
|
||||
7681/tcp: null
|
||||
8787/tcp: 8787
|
||||
8787/tcp: null
|
||||
ports_description:
|
||||
3001/tcp: Claude Desktop web interface
|
||||
7681/tcp: HTTP Basic-auth Claude Code terminal (no TLS)
|
||||
8787/tcp: Headroom dashboard and proxy
|
||||
8787/tcp: Optional Headroom dashboard and proxy
|
||||
privileged:
|
||||
- SYS_ADMIN
|
||||
- DAC_READ_SEARCH
|
||||
@@ -81,24 +84,33 @@ schema:
|
||||
TZ: match([A-Z][a-z]*./[A-Z][a-z]*.)?
|
||||
additional_apps: str?
|
||||
additional_pip: str?
|
||||
auto_update: bool?
|
||||
cifsdomain: str?
|
||||
cifspassword: str?
|
||||
cifsusername: str?
|
||||
localdisks: str?
|
||||
networkdisks: str?
|
||||
github_email: str?
|
||||
ha_smart_context: bool?
|
||||
enable_ha_mcp: bool?
|
||||
dangerously_skip_permissions: bool?
|
||||
enable_terminal: bool?
|
||||
terminal_username: match(^[A-Za-z0-9_.-]+$)?
|
||||
terminal_password: password?
|
||||
terminal_workspace: str?
|
||||
ha_mcp_url: str?
|
||||
ha_mcp_token: password?
|
||||
enable_ha_api_helper: bool?
|
||||
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.13"
|
||||
version: "1.31"
|
||||
video: true
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 5.3 KiB |
@@ -22,8 +22,7 @@ else
|
||||
fi
|
||||
|
||||
# Headroom is intentionally not injected into the Desktop process. Claude Desktop overrides
|
||||
# ANTHROPIC_BASE_URL, so Desktop uses the registered Headroom MCP tools instead. The Claude Code
|
||||
# terminal uses the supported `headroom wrap claude` integration through claude-headroom.
|
||||
# ANTHROPIC_BASE_URL, so Desktop uses the registered Headroom MCP tools instead.
|
||||
|
||||
# Launch the configured command. If a custom/wrapped command fails to start, fall back to
|
||||
# the plain Claude Desktop launch so the app always comes up for the user.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Hourly rtk + headroom token-savings report to the add-on log (heartbeat + gains).
|
||||
# Hourly RTK + Headroom + TokenSave savings report to the add-on log.
|
||||
# Seeded to /data/data/crontabs/root by init-crontab-config and run by svc-cron; edit the
|
||||
# persistent copy to customize. Output goes to /proc/1/fd/1 so it shows in the add-on log.
|
||||
0 * * * * /usr/local/bin/claude-gains-report.sh > /proc/1/fd/1 2>&1
|
||||
|
||||
@@ -3,9 +3,35 @@
|
||||
# shellcheck disable=SC2046
|
||||
set -e
|
||||
|
||||
# Define user
|
||||
PUID=$(bashio::config "PUID")
|
||||
PGID=$(bashio::config "PGID")
|
||||
# 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
|
||||
|
||||
# Check data location
|
||||
LOCATION="$(bashio::config 'data_location')"
|
||||
@@ -59,10 +85,23 @@ 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"
|
||||
grep -qxF "export HOME=\"$LOCATION\"" ~/.bashrc 2>/dev/null || {
|
||||
# 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"
|
||||
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"
|
||||
@@ -70,6 +109,11 @@ 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
|
||||
@@ -81,7 +125,7 @@ fi
|
||||
ln -sfn /tmp/cache "$LOCATION/.cache"
|
||||
|
||||
bashio::log.info "Setting ownership to $PUID:$PGID"
|
||||
chown -R "$PUID":"$PGID" "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR"
|
||||
chown -R "${PUID}:${PGID}" "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR" /data
|
||||
chmod -R 700 "$LOCATION"
|
||||
|
||||
# The base init-selkies-config script overrides XDG_RUNTIME_DIR to $HOME/.XDG, which lands
|
||||
|
||||
@@ -24,7 +24,12 @@ fi
|
||||
if bashio::config.has_value 'additional_pip'; then
|
||||
for p in $(bashio::config 'additional_pip' | tr ',' ' '); do
|
||||
bashio::log.green "... pip: $p"
|
||||
pip3 install --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed"
|
||||
# Prefer uv (much faster resolver/installer); fall back to pip3 when unavailable.
|
||||
if command -v uv &> /dev/null; then
|
||||
uv pip install --system --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed"
|
||||
else
|
||||
pip3 install --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
|
||||
if bashio::config.true 'auto_update'; then
|
||||
bashio::log.info "Checking for Claude Desktop updates..."
|
||||
if apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null && apt-get install -y --only-upgrade claude-desktop &> /dev/null; then
|
||||
bashio::log.info "Claude Desktop version: $(dpkg-query -W -f='${Version}' claude-desktop)"
|
||||
else
|
||||
bashio::log.warning "Update check failed (offline?), keeping current version"
|
||||
fi
|
||||
bashio::log.info "Checking for Claude Desktop updates..."
|
||||
if apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null && apt-get install -y --only-upgrade claude-desktop &> /dev/null; then
|
||||
bashio::log.info "Claude Desktop version: $(dpkg-query -W -f='${Version}' claude-desktop)"
|
||||
else
|
||||
bashio::log.warning "Update check failed (offline?), keeping current version"
|
||||
fi
|
||||
|
||||
42
claude_desktop/rootfs/etc/cont-init.d/81-tokensave_repositories.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
if ! bashio::config.true 'install_tokensave' || ! command -v git > /dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
declare -A REPOS_SEEN=()
|
||||
# bashio::config prints its result without a trailing newline, so the last record arrives
|
||||
# with read returning non-zero; the extra test keeps that final path in the loop.
|
||||
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
|
||||
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
|
||||
/*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
[ -d "$configured_path" ] || continue
|
||||
|
||||
# The one-shot safe.directory override is used only to discover the repository root.
|
||||
# Persist the resolved root in the shared runtime user's Git config before 82-claude_tools.sh
|
||||
# performs normal repository detection, avoiding Git's dubious-ownership rejection.
|
||||
repo_root="$(s6-setuidgid abc env HOME="$HOME" \
|
||||
git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
|
||||
[ -n "$repo_root" ] && [ "$repo_root" != "/" ] || continue
|
||||
[[ -z "${REPOS_SEEN[$repo_root]:-}" ]] || continue
|
||||
REPOS_SEEN[$repo_root]=1
|
||||
|
||||
if ! s6-setuidgid abc env HOME="$HOME" git config --global --get-all safe.directory \
|
||||
| grep -Fxq -- "$repo_root"; then
|
||||
s6-setuidgid abc env HOME="$HOME" git config --global --add safe.directory "$repo_root"
|
||||
bashio::log.info "Marked TokenSave repository as safe for Git: ${repo_root}"
|
||||
fi
|
||||
# bashio::config prints list options one entry per line ("null" when the key is absent);
|
||||
# bashio::config.array only exists in the repo's standalone bashio, not in the real bashio here.
|
||||
done < <(bashio::config 'tokensave_project_paths')
|
||||
@@ -3,73 +3,291 @@
|
||||
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)"
|
||||
# 20-folders.sh already remapped abc to the effective runtime identity (never root in bypass
|
||||
# mode), so follow abc instead of re-reading the raw PUID/PGID options here.
|
||||
RUNTIME_UID="$(id -u abc)"
|
||||
RUNTIME_GID="$(id -g abc)"
|
||||
mkdir -p "$HOME/.claude"
|
||||
|
||||
run_as_runtime_user() {
|
||||
s6-setuidgid abc env HOME="$HOME" "$@"
|
||||
}
|
||||
|
||||
CLAUDE_DESKTOP_COMMAND_FILE="/tmp/claude-desktop-command"
|
||||
DEFAULT_CLAUDE_DESKTOP_COMMAND='claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret'
|
||||
printf '%s\n' "$DEFAULT_CLAUDE_DESKTOP_COMMAND" > "$CLAUDE_DESKTOP_COMMAND_FILE"
|
||||
|
||||
# headroom's "wrap"/proxy routing works by setting ANTHROPIC_BASE_URL, which the Claude Desktop
|
||||
# Electron app force-overrides to the production endpoint (headroom #869), so transparent
|
||||
# compression cannot be applied to the desktop launch. The integration that does work with
|
||||
# Claude Desktop is headroom's MCP server, which exposes the headroom_compress/headroom_retrieve/
|
||||
# headroom_stats tools inside the app. Register it in Claude Desktop's MCP config, leaving the
|
||||
# plain launch untouched. The merge is idempotent and preserves any other MCP servers.
|
||||
# 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
|
||||
bashio::log.info "headroom $(headroom --version 2> /dev/null || true) available; registering the headroom MCP server for Claude Desktop"
|
||||
HEADROOM_BIN="$(command -v headroom)" CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" python3 - <<'PY' || bashio::log.warning "Unable to register the headroom MCP server automatically"
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(os.environ["CLAUDE_DESKTOP_CONFIG"])
|
||||
try:
|
||||
data = json.loads(path.read_text()) if path.exists() else {}
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
except Exception:
|
||||
if path.exists():
|
||||
path.rename(path.with_suffix(path.suffix + ".bak"))
|
||||
data = {}
|
||||
servers = data.get("mcpServers")
|
||||
if not isinstance(servers, dict):
|
||||
servers = {}
|
||||
data["mcpServers"] = servers
|
||||
servers["headroom"] = {"command": os.environ.get("HEADROOM_BIN", "headroom"), "args": ["mcp", "serve"]}
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
PY
|
||||
HEADROOM_ENABLED=true
|
||||
bashio::log.info "headroom $(headroom --version 2> /dev/null || true) available; registering the headroom MCP server"
|
||||
else
|
||||
bashio::log.warning "headroom is not available"
|
||||
fi
|
||||
elif [ -f "$CLAUDE_DESKTOP_CONFIG" ]; then
|
||||
bashio::log.info "Removing the headroom MCP server from Claude Desktop"
|
||||
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" python3 - <<'PY' || bashio::log.warning "Unable to remove the headroom MCP server automatically"
|
||||
fi
|
||||
|
||||
TOKENSAVE_ENABLED=false
|
||||
if bashio::config.true 'install_tokensave'; then
|
||||
if command -v tokensave &> /dev/null; then
|
||||
TOKENSAVE_ENABLED=true
|
||||
bashio::log.info "tokensave $(tokensave --version 2> /dev/null || true) available; configuring the complete Claude Code integration"
|
||||
# The upstream installer adds the MCP entry, PreToolUse/UserPromptSubmit/Stop hooks,
|
||||
# MCP permissions, global CLAUDE.md rules, and the global post-commit/checkout sync hook.
|
||||
run_as_runtime_user tokensave install --agent claude --git-hook yes \
|
||||
|| bashio::log.warning "tokensave Claude Code integration setup failed"
|
||||
else
|
||||
bashio::log.warning "tokensave is not available"
|
||||
fi
|
||||
elif command -v tokensave &> /dev/null; then
|
||||
bashio::log.info "Removing the tokensave Claude Code integration"
|
||||
run_as_runtime_user tokensave uninstall --agent claude \
|
||||
|| bashio::log.warning "tokensave Claude Code integration removal failed"
|
||||
fi
|
||||
|
||||
HA_MCP_ENABLED=false
|
||||
HA_MCP_URL=""
|
||||
HA_MCP_TOKEN=""
|
||||
if bashio::config.true 'enable_ha_mcp'; then
|
||||
HA_MCP_URL="$(bashio::config 'ha_mcp_url' 'http://homeassistant:8123/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"
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(os.environ["CLAUDE_DESKTOP_CONFIG"])
|
||||
data = json.loads(path.read_text())
|
||||
if isinstance(data, dict):
|
||||
MANAGED_BASENAMES = {
|
||||
"headroom": "headroom",
|
||||
"tokensave": "tokensave",
|
||||
"homeassistant": "mcp-proxy",
|
||||
}
|
||||
|
||||
desired = {}
|
||||
if os.environ["HEADROOM_ENABLED"] == "true":
|
||||
desired["headroom"] = {
|
||||
"command": os.environ["HEADROOM_BIN"],
|
||||
"args": ["mcp", "serve", "--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 = {}
|
||||
servers = data.get("mcpServers")
|
||||
if isinstance(servers, dict) and servers.pop("headroom", None) is not None:
|
||||
if not servers:
|
||||
data.pop("mcpServers", None)
|
||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
if not isinstance(servers, dict):
|
||||
servers = {}
|
||||
changed = False
|
||||
for name in MANAGED_BASENAMES:
|
||||
existing = servers.get(name)
|
||||
if name in desired:
|
||||
entry = dict(desired[name])
|
||||
if stdio_type:
|
||||
entry["type"] = "stdio"
|
||||
if existing is None or is_managed(name, existing):
|
||||
if existing != entry:
|
||||
servers[name] = entry
|
||||
changed = True
|
||||
elif existing is not None and is_managed(name, existing):
|
||||
del servers[name]
|
||||
changed = True
|
||||
if not changed:
|
||||
continue
|
||||
if servers:
|
||||
data["mcpServers"] = servers
|
||||
else:
|
||||
data.pop("mcpServers", None)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
# The Home Assistant long-lived access token is stored here in clear text.
|
||||
path.chmod(0o600)
|
||||
PY
|
||||
|
||||
# Initialize or incrementally sync only explicitly configured repositories. TokenSave deliberately
|
||||
# requires one-time per-project opt-in; an empty list therefore has no startup or storage cost.
|
||||
if $TOKENSAVE_ENABLED; then
|
||||
declare -A TOKENSAVE_REPOS_SEEN=()
|
||||
# bashio::config prints its result without a trailing newline, so the last record arrives
|
||||
# with read returning non-zero; the extra test keeps that final path in the loop.
|
||||
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
|
||||
|
||||
repo_root="$(git -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
|
||||
if [ -z "$repo_root" ] || [ "$repo_root" = "/" ]; then
|
||||
bashio::log.warning "Skipping TokenSave path that is not a supported Git repository: ${configured_path}"
|
||||
continue
|
||||
fi
|
||||
if [[ -n "${TOKENSAVE_REPOS_SEEN[$repo_root]:-}" ]]; then
|
||||
continue
|
||||
fi
|
||||
TOKENSAVE_REPOS_SEEN[$repo_root]=1
|
||||
|
||||
bashio::log.info "Preparing TokenSave index: ${repo_root}"
|
||||
# Prepare the per-repo semantic graph defensively so a hard add-on stop or storage
|
||||
# hiccup can never leave a broken index that fails every subsequent boot:
|
||||
# * a startup-scoped flock serializes against an overlapping restart (and any git
|
||||
# post-commit/checkout sync hook that fires mid-boot); 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 this script's final ownership pass.
|
||||
# shellcheck disable=SC2016 # single-quoted on purpose: $1/$db/etc. expand in the abc shell
|
||||
run_as_runtime_user bash -c '
|
||||
set -o pipefail
|
||||
repo_root="$1"
|
||||
ts_dir="$repo_root/.tokensave"
|
||||
db="$ts_dir/tokensave.db"
|
||||
lock="$ts_dir/.startup.lock"
|
||||
initflag="$ts_dir/.init-incomplete"
|
||||
mkdir -p "$ts_dir"
|
||||
exec 9>"$lock"
|
||||
if ! flock -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}"
|
||||
# bashio::config prints list options one entry per line ("null" when the key is absent);
|
||||
# bashio::config.array only exists in the repo's standalone bashio, not in the real bashio here.
|
||||
done < <(bashio::config 'tokensave_project_paths')
|
||||
fi
|
||||
|
||||
# Guide Claude to actually use the headroom compression tools so the MCP integration produces
|
||||
# real savings (otherwise the tools sit unused and `headroom savings` stays empty). Managed,
|
||||
# idempotent block appended to the user's global CLAUDE.md; removed when headroom is disabled.
|
||||
# Guide Claude to actually use the Headroom compression tools so the MCP integration produces
|
||||
# real savings when transparent proxying is unavailable. Managed, idempotent block appended to
|
||||
# the user's global CLAUDE.md; removed when Headroom is disabled.
|
||||
CLAUDE_MD="$HOME/.claude/CLAUDE.md"
|
||||
HEADROOM_GUIDE_BEGIN="<!-- BEGIN headroom (managed by claude_desktop addon) -->"
|
||||
if bashio::config.true 'install_headroom'; then
|
||||
if $HEADROOM_ENABLED; then
|
||||
mkdir -p "$(dirname "$CLAUDE_MD")"
|
||||
if ! { [ -f "$CLAUDE_MD" ] && grep -qF "$HEADROOM_GUIDE_BEGIN" "$CLAUDE_MD"; }; then
|
||||
bashio::log.info "Adding headroom usage guidance to CLAUDE.md"
|
||||
@@ -110,16 +328,213 @@ if new != text:
|
||||
PY
|
||||
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
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
MANAGED_URL = "http://127.0.0.1:8787"
|
||||
|
||||
path = Path.home() / ".claude" / "settings.json"
|
||||
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 = {}
|
||||
|
||||
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")
|
||||
PY
|
||||
|
||||
# Compress large tool outputs automatically in every Claude Code session via a managed
|
||||
# PostToolUse hook (settings.json hooks apply to terminal, cowork, dispatch and cron sessions
|
||||
# alike). 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
|
||||
HEADROOM_HOOK_ACTION="$HEADROOM_HOOK_ACTION" HEADROOM_HOOK_CMD="$HEADROOM_HOOK_CMD" \
|
||||
HEADROOM_HOOK_MATCHER="Bash|Grep|Glob|WebFetch" \
|
||||
python3 - <<'PY' || bashio::log.warning "Unable to manage the Headroom auto-compression hook"
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
action = os.environ["HEADROOM_HOOK_ACTION"]
|
||||
command = os.environ["HEADROOM_HOOK_CMD"]
|
||||
matcher = os.environ["HEADROOM_HOOK_MATCHER"]
|
||||
|
||||
path = Path.home() / ".claude" / "settings.json"
|
||||
original = path.read_text() if path.exists() else None
|
||||
try:
|
||||
data = json.loads(original) if original is not None 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
|
||||
data = {}
|
||||
|
||||
hooks = data.get("hooks") if isinstance(data.get("hooks"), dict) else {}
|
||||
entries = hooks.get("PostToolUse") if isinstance(hooks.get("PostToolUse"), list) else []
|
||||
|
||||
# Strip the managed command everywhere first, then re-append when enabled: the same pass
|
||||
# handles removal, de-duplication, and matcher migration on version upgrades. The final
|
||||
# text comparison keeps the write idempotent across boots.
|
||||
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["PostToolUse"] = entries
|
||||
else:
|
||||
hooks.pop("PostToolUse", 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)
|
||||
PY
|
||||
|
||||
# Tell Claude Code that it can configure Home Assistant over the Core API via the shipped
|
||||
# `ha-cli` helper (no /config filesystem mount needed). Managed, idempotent block appended to
|
||||
# the user's global CLAUDE.md; removed when the helper is disabled. Mirrors the headroom block.
|
||||
HA_HELPER_GUIDE_BEGIN="<!-- BEGIN ha-api-helper (managed by claude_desktop addon) -->"
|
||||
if bashio::config.true 'enable_ha_api_helper'; then
|
||||
mkdir -p "$(dirname "$CLAUDE_MD")"
|
||||
if ! { [ -f "$CLAUDE_MD" ] && grep -qF "$HA_HELPER_GUIDE_BEGIN" "$CLAUDE_MD"; }; then
|
||||
bashio::log.info "Adding Home Assistant API helper guidance to CLAUDE.md"
|
||||
{
|
||||
[ -s "$CLAUDE_MD" ] && printf '\n'
|
||||
cat <<'MD'
|
||||
<!-- BEGIN ha-api-helper (managed by claude_desktop addon) -->
|
||||
## Configuring Home Assistant
|
||||
|
||||
You can configure this Home Assistant instance through its Core API using the `ha-cli`
|
||||
command (on `PATH`). It authenticates automatically with the add-on's `$SUPERVISOR_TOKEN`,
|
||||
so no token setup is needed. There is **no `/config` filesystem mount** — work only through
|
||||
`ha-cli`, and never try to read or write Home Assistant YAML files directly.
|
||||
|
||||
What is editable this way: automations, scripts, and scenes
|
||||
(`ha-cli get|post|delete config/automation/config/<id>` and the `script`/`scene` equivalents);
|
||||
service calls (`ha-cli call <domain.service> '<json>'`); state reads (`ha-cli states`); and,
|
||||
over WebSocket, helpers, dashboards, and area/label/floor/entity registries
|
||||
(`ha-cli ws '{"type":"..."}'`). Run `ha-cli --help` for the full reference. Raw YAML
|
||||
(`configuration.yaml`, `secrets.yaml`) is intentionally unreachable — if a change needs it,
|
||||
say so instead of working around it.
|
||||
|
||||
Rules: run `ha-cli config` first to confirm connectivity; **read the current object and show
|
||||
the user the intended change, then wait for confirmation** before any create/update/delete or
|
||||
any state-changing `call`; after writing, read the object back and reload if needed
|
||||
(e.g. `ha-cli call automation.reload`).
|
||||
<!-- END ha-api-helper (managed by claude_desktop addon) -->
|
||||
MD
|
||||
} >> "$CLAUDE_MD"
|
||||
fi
|
||||
elif [ -f "$CLAUDE_MD" ] && grep -qF "$HA_HELPER_GUIDE_BEGIN" "$CLAUDE_MD"; then
|
||||
bashio::log.info "Removing Home Assistant API helper guidance from CLAUDE.md"
|
||||
CLAUDE_MD="$CLAUDE_MD" python3 - <<'PY' || bashio::log.warning "Unable to remove Home Assistant API helper guidance automatically"
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(os.environ["CLAUDE_MD"])
|
||||
text = path.read_text(encoding="utf-8")
|
||||
pattern = re.compile(
|
||||
r"\n*<!-- BEGIN ha-api-helper \(managed by claude_desktop addon\) -->.*?"
|
||||
r"<!-- END ha-api-helper \(managed by claude_desktop addon\) -->\n?",
|
||||
re.DOTALL,
|
||||
)
|
||||
new = pattern.sub("", text)
|
||||
if new != text:
|
||||
path.write_text(new, encoding="utf-8")
|
||||
PY
|
||||
fi
|
||||
|
||||
if bashio::config.true 'install_rtk'; then
|
||||
if command -v rtk &> /dev/null; then
|
||||
if [ -f "$HOME/.claude/settings.json" ] && grep -q 'rtk hook claude' "$HOME/.claude/settings.json"; then
|
||||
bashio::log.info "rtk Claude Code hook already configured"
|
||||
else
|
||||
bashio::log.info "Configuring rtk Claude Code hook"
|
||||
RTK_NONINTERACTIVE=1 rtk init -g || bashio::log.warning "rtk global files configuration failed"
|
||||
python3 - <<'PY' || bashio::log.warning "Unable to configure rtk hook automatically"
|
||||
bashio::log.info "Configuring rtk Claude Code integration"
|
||||
run_as_runtime_user env RTK_NONINTERACTIVE=1 rtk init -g \
|
||||
|| bashio::log.warning "rtk global files configuration failed"
|
||||
python3 - <<'PY' || bashio::log.warning "Unable to configure rtk hook automatically"
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
path = Path.home() / ".claude" / "settings.json"
|
||||
try:
|
||||
data = json.loads(path.read_text()) if path.exists() else {}
|
||||
@@ -137,7 +552,6 @@ if not any("rtk hook claude" in json.dumps(entry) for entry in pre if isinstance
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
PY
|
||||
fi
|
||||
else
|
||||
bashio::log.warning "rtk is not available"
|
||||
fi
|
||||
@@ -209,17 +623,18 @@ 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
|
||||
|
||||
# Startup configuration runs as root, while Claude Desktop and the web terminal run as abc.
|
||||
# Return managed persistent files to the configured runtime UID/GID after all writes complete.
|
||||
for managed_path in "$HOME/.claude" "$HOME/.config/Claude"; do
|
||||
# Startup configuration runs as root, while Claude Desktop runs as abc. Return managed
|
||||
# persistent files to the effective runtime UID/GID after all writes complete.
|
||||
for managed_path in "$HOME/.claude" "$HOME/.claude.json" "$HOME/.config/Claude"; do
|
||||
if [ -e "$managed_path" ]; then
|
||||
chown -R -- "${PUID}:${PGID}" "$managed_path" || bashio::log.warning "Unable to set ownership on $managed_path"
|
||||
chown -R -- "${RUNTIME_UID}:${RUNTIME_GID}" "$managed_path" || bashio::log.warning "Unable to set ownership on $managed_path"
|
||||
fi
|
||||
done
|
||||
|
||||
94
claude_desktop/rootfs/etc/cont-init.d/83-claude_permissions.sh
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
# 20-folders.sh already remapped abc to the effective runtime identity (never root in bypass
|
||||
# mode), so follow abc instead of re-reading the raw PUID/PGID options here.
|
||||
RUNTIME_UID="$(id -u abc)"
|
||||
RUNTIME_GID="$(id -g abc)"
|
||||
PERMISSION_MODE="$(bashio::config 'permission_mode')"
|
||||
SETTINGS_PATH="$HOME/.claude/settings.json"
|
||||
STATE_PATH="$HOME/.claude/.addon-permission-mode.json"
|
||||
|
||||
case "$PERMISSION_MODE" in
|
||||
strict|auto|bypass) ;;
|
||||
*)
|
||||
bashio::log.warning "Unknown permission_mode '${PERMISSION_MODE}'; falling back to strict"
|
||||
PERMISSION_MODE="strict"
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "$(dirname "$SETTINGS_PATH")"
|
||||
PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" STATE_PATH="$STATE_PATH" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
mode = os.environ["PERMISSION_MODE"]
|
||||
settings_path = Path(os.environ["SETTINGS_PATH"])
|
||||
state_path = Path(os.environ["STATE_PATH"])
|
||||
|
||||
try:
|
||||
settings = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
||||
except (OSError, json.JSONDecodeError):
|
||||
if settings_path.exists():
|
||||
settings_path.rename(settings_path.with_suffix(settings_path.suffix + ".bak"))
|
||||
settings = {}
|
||||
if not isinstance(settings, dict):
|
||||
settings = {}
|
||||
|
||||
try:
|
||||
state = json.loads(state_path.read_text()) if state_path.exists() else None
|
||||
except (OSError, json.JSONDecodeError):
|
||||
state = None
|
||||
if not isinstance(state, dict):
|
||||
state = None
|
||||
|
||||
permissions = settings.get("permissions")
|
||||
if not isinstance(permissions, dict):
|
||||
permissions = {}
|
||||
|
||||
if mode == "strict":
|
||||
# Restore the value that existed before the add-on first managed this setting.
|
||||
if state is not None:
|
||||
if state.get("previous_exists"):
|
||||
permissions["defaultMode"] = state.get("previous_value")
|
||||
else:
|
||||
permissions.pop("defaultMode", None)
|
||||
state_path.unlink(missing_ok=True)
|
||||
else:
|
||||
if state is None:
|
||||
state = {
|
||||
"previous_exists": "defaultMode" in permissions,
|
||||
"previous_value": permissions.get("defaultMode"),
|
||||
}
|
||||
state_path.write_text(json.dumps(state, indent=2) + "\n")
|
||||
state_path.chmod(0o600)
|
||||
permissions["defaultMode"] = "auto" if mode == "auto" else "bypassPermissions"
|
||||
|
||||
if permissions:
|
||||
settings["permissions"] = permissions
|
||||
else:
|
||||
settings.pop("permissions", None)
|
||||
|
||||
settings_path.write_text(json.dumps(settings, indent=2) + "\n")
|
||||
settings_path.chmod(0o600)
|
||||
PY
|
||||
|
||||
case "$PERMISSION_MODE" in
|
||||
strict)
|
||||
bashio::log.info "Claude Code permission mode: strict (normal prompts)"
|
||||
;;
|
||||
auto)
|
||||
bashio::log.info "Claude Code permission mode: auto (safe actions approved automatically)"
|
||||
;;
|
||||
bypass)
|
||||
bashio::log.warning "Claude Code permission mode: bypass (permission checks disabled for mounted data and available tools)"
|
||||
;;
|
||||
esac
|
||||
|
||||
chown -- "${RUNTIME_UID}:${RUNTIME_GID}" "$SETTINGS_PATH" 2> /dev/null || true
|
||||
if [ -e "$STATE_PATH" ]; then
|
||||
chown -- "${RUNTIME_UID}:${RUNTIME_GID}" "$STATE_PATH" 2> /dev/null || true
|
||||
fi
|
||||
@@ -17,25 +17,54 @@ 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
|
||||
git config --global user.name "$(bashio::config 'github_username')"
|
||||
run_as_runtime_user git config --global user.name "$(bashio::config 'github_username')"
|
||||
fi
|
||||
|
||||
if bashio::config.has_value 'github_email'; then
|
||||
git config --global user.email "$(bashio::config 'github_email')"
|
||||
run_as_runtime_user git config --global user.email "$(bashio::config 'github_email')"
|
||||
fi
|
||||
|
||||
if bashio::config.has_value 'github_token'; then
|
||||
token="$(bashio::config 'github_token')"
|
||||
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
|
||||
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
|
||||
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" | 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" | 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"
|
||||
fi
|
||||
env -u GH_TOKEN -u GITHUB_TOKEN gh auth setup-git --hostname github.com || bashio::log.warning "GitHub CLI git credential setup failed"
|
||||
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"
|
||||
else
|
||||
bashio::log.info "GitHub CLI available. Set github_token to authenticate gh and git operations."
|
||||
fi
|
||||
|
||||
17
claude_desktop/rootfs/etc/cont-init.d/84-claude_runtime_ownership.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/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
|
||||
@@ -4,34 +4,105 @@ set -e
|
||||
|
||||
NGINX_CONFIG=/etc/nginx/sites-available/ingress.conf
|
||||
SUBFOLDER="$(bashio::addon.ingress_entry)"
|
||||
INGRESS_PORT="$(bashio::addon.ingress_port)"
|
||||
DOWNLOADS_PATH="${HOME:-/config}"
|
||||
|
||||
# Ensure subfolder ends with a trailing slash (except for root)
|
||||
# Home Assistant normally strips the ingress prefix before forwarding to the add-on,
|
||||
# but keep the normalized value available for diagnostics and future-safe logging.
|
||||
if [[ -n "${SUBFOLDER}" && "${SUBFOLDER}" != "/" ]]; then
|
||||
[[ "${SUBFOLDER}" == */ ]] || SUBFOLDER="${SUBFOLDER}/"
|
||||
else
|
||||
SUBFOLDER="/"
|
||||
fi
|
||||
|
||||
cp /defaults/default.conf "${NGINX_CONFIG}"
|
||||
# Claude Desktop exposes only 3001/tcp in config.yaml. Older Supervisor/bashio
|
||||
# combinations can return an empty ingress_port when it is not explicit, which would
|
||||
# make nginx write an invalid `listen` directive. Fall back to the declared port.
|
||||
if [[ -z "${INGRESS_PORT}" ]]; then
|
||||
INGRESS_PORT="3001"
|
||||
fi
|
||||
|
||||
# Keep only the first (non-SSL) server block
|
||||
awk -v n=2 '/^[[:space:]]*server[[:space:]]*\{/{n--} n>0' "${NGINX_CONFIG}" > tmpfile
|
||||
mv tmpfile "${NGINX_CONFIG}"
|
||||
DOWNLOADS_PATH="${DOWNLOADS_PATH%/}"
|
||||
|
||||
# Disable IPv6 listeners for ingress proxying
|
||||
sed -i '/listen \[::\]/d' "${NGINX_CONFIG}"
|
||||
cat > "${NGINX_CONFIG}" <<EOF
|
||||
server {
|
||||
listen ${INGRESS_PORT} default_server;
|
||||
client_max_body_size 10M;
|
||||
|
||||
# Adapt ports and upstream paths for Home Assistant ingress
|
||||
sed -i "s|3000|$(bashio::addon.ingress_port)|g" "${NGINX_CONFIG}"
|
||||
sed -i "s|SUBFOLDER|/|g" "${NGINX_CONFIG}"
|
||||
sed -i "s|CWS|8082|g" "${NGINX_CONFIG}"
|
||||
sed -i "s|REPLACE_HOME|${HOME:-/root}|g" "${NGINX_CONFIG}"
|
||||
sed -i "s|REPLACE_DOWNLOADS_PATH|${HOME:-/config}|g" "${NGINX_CONFIG}"
|
||||
sed -i '/proxy_buffering/a proxy_set_header Accept-Encoding "";' "${NGINX_CONFIG}"
|
||||
sed -i '/proxy_buffering/a sub_filter_once off;' "${NGINX_CONFIG}"
|
||||
sed -i '/proxy_buffering/a sub_filter_types *;' "${NGINX_CONFIG}"
|
||||
sed -i '/proxy_buffering/a sub_filter "vnc/index.html?autoconnect" "vnc/index.html?path=%%path%%/websockify?autoconnect";' "${NGINX_CONFIG}"
|
||||
sed -i "s|%%path%%|${SUBFOLDER:1}|g" "${NGINX_CONFIG}"
|
||||
location / {
|
||||
alias /usr/share/selkies/web/;
|
||||
index index.html index.htm;
|
||||
try_files \$uri \$uri/ /index.html;
|
||||
}
|
||||
|
||||
location /devmode {
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_http_version 1.1;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
proxy_pass http://127.0.0.1:5173;
|
||||
}
|
||||
|
||||
# Current Selkies WebSocket mode connects to <base>/api/websockets.
|
||||
# The older linuxserver default.conf only proxies /websocket, leaving the
|
||||
# dashboard loaded but stuck on "waiting for stream" under Home Assistant ingress.
|
||||
location /api/ {
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_http_version 1.1;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
proxy_pass http://127.0.0.1:8082;
|
||||
}
|
||||
|
||||
# Keep compatibility with older Selkies/noVNC clients and linuxserver templates.
|
||||
location /websocket {
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_http_version 1.1;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
proxy_pass http://127.0.0.1:8082;
|
||||
}
|
||||
|
||||
location /files {
|
||||
fancyindex on;
|
||||
fancyindex_footer /nginx/footer.html;
|
||||
fancyindex_header /nginx/header.html;
|
||||
alias ${DOWNLOADS_PATH}/;
|
||||
if (-f \$request_filename) {
|
||||
add_header Content-Disposition "attachment";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
}
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/selkies/web/;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Avoid content encoding on proxied responses to keep Selkies happy (handled by proxy_set_header Accept-Encoding insertion above)
|
||||
cp "${NGINX_CONFIG}" /etc/nginx/sites-enabled
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
declare port=7681
|
||||
declare username
|
||||
declare password=""
|
||||
declare workspace
|
||||
declare canonical_workspace
|
||||
|
||||
export PATH="${HOME:-/data/data}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
|
||||
|
||||
if bashio::config.has_value 'enable_terminal' && ! bashio::config.true 'enable_terminal'; then
|
||||
bashio::log.info "svc-claude-terminal: terminal disabled; idling"
|
||||
exec sleep infinity
|
||||
fi
|
||||
|
||||
if [ -z "${HOME:-}" ]; then
|
||||
bashio::log.error "svc-claude-terminal: HOME is not initialized; idling"
|
||||
exec sleep infinity
|
||||
fi
|
||||
|
||||
if ! command -v ttyd >/dev/null 2>&1 || ! command -v tmux >/dev/null 2>&1 || ! command -v claude >/dev/null 2>&1; then
|
||||
bashio::log.error "svc-claude-terminal: ttyd, tmux, or Claude Code is missing; idling"
|
||||
exec sleep infinity
|
||||
fi
|
||||
|
||||
username="claude"
|
||||
if bashio::config.has_value 'terminal_username'; then
|
||||
username="$(bashio::config 'terminal_username')"
|
||||
fi
|
||||
|
||||
if bashio::config.has_value 'terminal_password'; then
|
||||
password="$(bashio::config 'terminal_password')"
|
||||
elif bashio::config.has_value 'PASSWORD'; then
|
||||
bashio::log.warning "svc-claude-terminal: using PASSWORD as fallback for terminal authentication; prefer a unique terminal_password"
|
||||
password="$(bashio::config 'PASSWORD')"
|
||||
fi
|
||||
|
||||
if [ -z "$password" ]; then
|
||||
bashio::log.warning "svc-claude-terminal: set terminal_password (or PASSWORD) before mapping port ${port}; terminal will remain disabled"
|
||||
exec sleep infinity
|
||||
fi
|
||||
|
||||
workspace="${HOME}/workspace"
|
||||
if bashio::config.has_value 'terminal_workspace'; then
|
||||
workspace="$(bashio::config 'terminal_workspace')"
|
||||
fi
|
||||
|
||||
if [[ "$workspace" != /* ]]; then
|
||||
bashio::log.error "svc-claude-terminal: terminal_workspace must be an absolute path; idling"
|
||||
exec sleep infinity
|
||||
fi
|
||||
|
||||
if [ -L "$workspace" ]; then
|
||||
bashio::log.error "svc-claude-terminal: terminal_workspace must not be a symbolic link; idling"
|
||||
exec sleep infinity
|
||||
fi
|
||||
|
||||
if ! canonical_workspace="$(realpath -m -- "$workspace")"; then
|
||||
bashio::log.error "svc-claude-terminal: unable to resolve terminal_workspace '$workspace'; idling"
|
||||
exec sleep infinity
|
||||
fi
|
||||
workspace="$canonical_workspace"
|
||||
|
||||
case "$workspace" in
|
||||
"$HOME" | "$HOME"/* | /share/* | /media/* | /mnt/* | /data/* | /config/*)
|
||||
;;
|
||||
*)
|
||||
bashio::log.error "svc-claude-terminal: terminal_workspace must be the configured data_location or a subdirectory of /share, /media, /mnt, /data, or /config; idling"
|
||||
exec sleep infinity
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ ! -e "$workspace" ]; then
|
||||
if ! install -d -m 0750 -o abc -g abc -- "$workspace"; then
|
||||
bashio::log.error "svc-claude-terminal: failed to create workspace '$workspace'; idling"
|
||||
exec sleep infinity
|
||||
fi
|
||||
elif [ ! -d "$workspace" ]; then
|
||||
bashio::log.error "svc-claude-terminal: terminal_workspace '$workspace' is not a directory; idling"
|
||||
exec sleep infinity
|
||||
fi
|
||||
|
||||
if ! s6-setuidgid abc test -r "$workspace" ||
|
||||
! s6-setuidgid abc test -w "$workspace" ||
|
||||
! s6-setuidgid abc test -x "$workspace"; then
|
||||
bashio::log.error "svc-claude-terminal: workspace '$workspace' must be readable, writable, and searchable by user abc; idling"
|
||||
exec sleep infinity
|
||||
fi
|
||||
|
||||
export CLAUDE_TERMINAL_WORKSPACE="$workspace"
|
||||
|
||||
bashio::log.info "svc-claude-terminal: starting authenticated ttyd terminal on port ${port}; workspace=${workspace}"
|
||||
exec s6-setuidgid abc ttyd \
|
||||
-p "$port" \
|
||||
-W \
|
||||
-O \
|
||||
-c "${username}:${password}" \
|
||||
/usr/local/bin/claude-terminal-shell
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -3,7 +3,28 @@
|
||||
declare port=8787
|
||||
declare host=127.0.0.1
|
||||
|
||||
if bashio::config.true 'install_headroom' && command -v headroom >/dev/null 2>&1; then
|
||||
# The dashboard is unauthenticated. Keep it container-local by default and bind all
|
||||
# interfaces only when the user explicitly opts in and maps port 8787.
|
||||
if bashio::config.true 'expose_headroom_dashboard'; then
|
||||
host=0.0.0.0
|
||||
fi
|
||||
|
||||
if bashio::config.true 'install_headroom' && command -v headroom > /dev/null 2>&1; then
|
||||
# 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
|
||||
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
|
||||
|
||||
53
claude_desktop/rootfs/usr/local/bin/claude
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -o pipefail
|
||||
|
||||
REAL_CLAUDE="/usr/bin/claude"
|
||||
HEADROOM_BIN="$(command -v headroom || true)"
|
||||
HEADROOM_URL="http://127.0.0.1:8787"
|
||||
PERMISSION_MODE="$(bashio::config 'permission_mode')"
|
||||
declare -a CLAUDE_PERMISSION_ARGS=()
|
||||
|
||||
case "$PERMISSION_MODE" in
|
||||
bypass)
|
||||
CLAUDE_PERMISSION_ARGS+=("--dangerously-skip-permissions")
|
||||
;;
|
||||
auto)
|
||||
CLAUDE_PERMISSION_ARGS+=("--permission-mode" "auto")
|
||||
;;
|
||||
strict|"")
|
||||
;;
|
||||
*)
|
||||
echo "claude wrapper: unknown permission_mode '${PERMISSION_MODE}', using strict mode" >&2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ ! -x "$REAL_CLAUDE" ]; then
|
||||
echo "claude wrapper: ${REAL_CLAUDE} is unavailable" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
# Claude Code rejects bypass mode when the effective UID is 0. Normal Desktop sessions run
|
||||
# as abc, which startup remaps to a non-root UID when bypass is selected. Also handle a user
|
||||
# invoking this wrapper directly from a root container console by dropping to abc here.
|
||||
if [ "$PERMISSION_MODE" = "bypass" ] && [ "$(id -u)" -eq 0 ]; then
|
||||
if command -v s6-setuidgid > /dev/null 2>&1 && [ "$(id -u abc)" -ne 0 ]; then
|
||||
exec s6-setuidgid abc "$0" "$@"
|
||||
fi
|
||||
echo "claude wrapper: bypass mode requires a non-root runtime user, but abc is still UID 0" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if bashio::config.true 'install_headroom' && bashio::config.true 'headroom_wrap_claude_code'; then
|
||||
if [ -x "$HEADROOM_BIN" ] && curl -fsS --max-time 2 "${HEADROOM_URL}/health" > /dev/null 2>&1; then
|
||||
# Put /usr/bin before /usr/local/bin while Headroom resolves its upstream `claude`
|
||||
# executable; otherwise it would resolve this wrapper recursively.
|
||||
export HEADROOM_CONTEXT_TOOL="rtk"
|
||||
exec env PATH="/usr/bin:/bin:/usr/local/bin" \
|
||||
"$HEADROOM_BIN" wrap claude --no-proxy -- \
|
||||
"${CLAUDE_PERMISSION_ARGS[@]}" "$@"
|
||||
fi
|
||||
echo "claude wrapper: Headroom proxy is unavailable; launching Claude Code directly" >&2
|
||||
fi
|
||||
|
||||
exec "$REAL_CLAUDE" "${CLAUDE_PERMISSION_ARGS[@]}" "$@"
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
exec claude "$@"
|
||||
@@ -1,34 +1,44 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# Hourly rtk + headroom token-savings snapshot for the add-on log.
|
||||
# Invoked by cron (see /defaults/crontabs/root); its stdout is redirected to /proc/1/fd/1,
|
||||
# so the report appears in the add-on log. Doubles as a heartbeat: if the numbers stop
|
||||
# growing, the corresponding tool has stopped working.
|
||||
# with-contenv supplies HOME from the s6 envdir, so this honors a custom `data_location`
|
||||
# (see 20-folders.sh) instead of hardcoding /data/data; it also makes bashio::config
|
||||
# available for the install_headroom gate below.
|
||||
export NO_COLOR=1 # keep the add-on log free of ANSI color codes
|
||||
# Hourly RTK + Headroom + TokenSave token-savings snapshot for the add-on log.
|
||||
# Invoked by cron (see /defaults/crontabs/root); stdout is redirected to /proc/1/fd/1.
|
||||
# Each tool is reported independently so enabling Headroom cannot hide RTK or TokenSave data.
|
||||
# with-contenv supplies the configured persistent HOME.
|
||||
export NO_COLOR=1
|
||||
export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
|
||||
|
||||
have_rtk=false; command -v rtk >/dev/null 2>&1 && have_rtk=true
|
||||
have_headroom=false; command -v headroom >/dev/null 2>&1 && have_headroom=true
|
||||
|
||||
# headroom is pip-installed unconditionally at build time, so its binary is on PATH even
|
||||
# when install_headroom is off — gate on the same config svc-headroom checks, and only
|
||||
# fall back to have_headroom as a secondary availability guard.
|
||||
headroom_enabled=false
|
||||
if bashio::config.true 'install_headroom' && $have_headroom; then
|
||||
headroom_enabled=true
|
||||
if ! bashio::config.true 'enable_tools_health_report'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Nothing to report if neither tool is active — stay quiet.
|
||||
if ! $have_rtk && ! $headroom_enabled; then exit 0; fi
|
||||
rtk_enabled=false
|
||||
headroom_enabled=false
|
||||
tokensave_enabled=false
|
||||
|
||||
echo "===== claude gains report $(date '+%Y-%m-%d %H:%M:%S') ====="
|
||||
if bashio::config.true 'install_rtk' && command -v rtk > /dev/null 2>&1; then
|
||||
rtk_enabled=true
|
||||
fi
|
||||
if bashio::config.true 'install_headroom' && command -v headroom > /dev/null 2>&1; then
|
||||
headroom_enabled=true
|
||||
fi
|
||||
if bashio::config.true 'install_tokensave' && command -v tokensave > /dev/null 2>&1; then
|
||||
tokensave_enabled=true
|
||||
fi
|
||||
|
||||
if ! $rtk_enabled && ! $headroom_enabled && ! $tokensave_enabled; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "===== claude tools report $(date '+%Y-%m-%d %H:%M:%S') ====="
|
||||
if $headroom_enabled; then
|
||||
echo "--- headroom savings ---"
|
||||
headroom savings 2>&1 || echo "[warn] headroom savings failed"
|
||||
elif $have_rtk; then
|
||||
fi
|
||||
if $rtk_enabled; then
|
||||
echo "--- rtk gain ---"
|
||||
rtk gain 2>&1 || echo "[warn] rtk gain failed"
|
||||
fi
|
||||
echo "===== end gains report ====="
|
||||
if $tokensave_enabled; then
|
||||
echo "--- tokensave gain ---"
|
||||
tokensave gain --all --range 30d 2>&1 || echo "[warn] tokensave gain failed"
|
||||
fi
|
||||
echo "===== end claude tools report ====="
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v claude >/dev/null 2>&1; then
|
||||
echo "Claude Code is not installed or is not on PATH." >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
if ! command -v headroom >/dev/null 2>&1; then
|
||||
echo "Headroom is unavailable; start Claude Code directly with claude-direct." >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
if ! curl -fsS --max-time 3 "http://127.0.0.1:8787/readyz" >/dev/null; then
|
||||
echo "The supervised Headroom proxy is not ready on 127.0.0.1:8787. Ensure install_headroom is enabled and check the add-on log." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Reuse the s6-supervised proxy instead of starting a competing proxy. RTK is already managed
|
||||
# through the persistent Claude Code PreToolUse hook, so Headroom must not reinstall it.
|
||||
exec headroom wrap claude --port 8787 --no-proxy --no-rtk -- "$@"
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${HOME:-}" ]; then
|
||||
echo "Claude terminal: HOME is not initialized." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export SHELL="/bin/bash"
|
||||
export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
|
||||
|
||||
workspace="${CLAUDE_TERMINAL_WORKSPACE:-${HOME}/workspace}"
|
||||
session_name="${CLAUDE_TMUX_SESSION:-claude}"
|
||||
|
||||
if [ ! -d "$workspace" ]; then
|
||||
echo "Claude terminal: workspace does not exist: $workspace" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd -- "$workspace"
|
||||
|
||||
# Reattach every browser connection to the same terminal session. Closing the browser detaches
|
||||
# the client but leaves Claude Code and other commands running inside tmux.
|
||||
exec tmux new-session -A -s "$session_name" -c "$workspace"
|
||||
176
claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh
Executable file
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# Diagnose installation, registration, routing, indexing, permissions, and recorded savings without
|
||||
# printing MCP environment values (which may contain the Home Assistant access token).
|
||||
# shellcheck shell=bash
|
||||
set +e
|
||||
set -o pipefail
|
||||
export NO_COLOR=1
|
||||
export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
|
||||
|
||||
section() {
|
||||
printf '\n=== %s ===\n' "$1"
|
||||
}
|
||||
|
||||
section "Installed binaries"
|
||||
for tool in claude claude-desktop headroom rtk tokensave git gh rg jq shellcheck yamllint hadolint actionlint; do
|
||||
resolved="$(command -v "$tool" 2> /dev/null || true)"
|
||||
if [ -n "$resolved" ]; then
|
||||
printf '%-16s %s\n' "$tool" "$resolved"
|
||||
else
|
||||
printf '%-16s %s\n' "$tool" "MISSING"
|
||||
fi
|
||||
done
|
||||
|
||||
section "Configured switches"
|
||||
for option in permission_mode install_headroom headroom_wrap_claude_code expose_headroom_dashboard install_rtk install_tokensave install_caveman enable_tools_health_report; do
|
||||
printf '%-30s %s\n' "$option" "$(bashio::config "$option")"
|
||||
done
|
||||
|
||||
section "Runtime identity"
|
||||
printf '%-30s %s\n' "configured PUID:PGID" "$(bashio::config 'PUID'):$(bashio::config 'PGID')"
|
||||
printf '%-30s %s\n' "effective abc UID:GID" "$(id -u abc):$(id -g abc)"
|
||||
printf '%-30s %s\n' "current process UID:GID" "$(id -u):$(id -g)"
|
||||
if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
|
||||
if [ "$(id -u abc)" -eq 0 ]; then
|
||||
echo "bypass runtime: ERROR - Claude Code will reject bypass permissions while abc is root"
|
||||
else
|
||||
echo "bypass runtime: OK - Claude Desktop and Cowork run as a non-root UID"
|
||||
fi
|
||||
fi
|
||||
|
||||
section "Claude Code permission state"
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
path = Path.home() / ".claude/settings.json"
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except FileNotFoundError:
|
||||
print("settings: MISSING")
|
||||
except Exception as exc:
|
||||
print(f"settings: INVALID: {exc}")
|
||||
else:
|
||||
permissions = data.get("permissions", {})
|
||||
if isinstance(permissions, dict):
|
||||
print(f"permissions.defaultMode: {permissions.get('defaultMode', '<upstream default>')}")
|
||||
else:
|
||||
print("permissions: INVALID")
|
||||
print(f"managed-state marker: {(Path.home() / '.claude/.addon-permission-mode.json').exists()}")
|
||||
PY
|
||||
|
||||
section "MCP registrations (environment values redacted)"
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
paths = [
|
||||
Path.home() / ".claude.json",
|
||||
Path.home() / ".config/Claude/claude_desktop_config.json",
|
||||
]
|
||||
for path in paths:
|
||||
print(path)
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except FileNotFoundError:
|
||||
print(" MISSING")
|
||||
continue
|
||||
except Exception as exc:
|
||||
print(f" INVALID: {exc}")
|
||||
continue
|
||||
servers = data.get("mcpServers", {})
|
||||
if not isinstance(servers, dict) or not servers:
|
||||
print(" no MCP servers")
|
||||
continue
|
||||
for name, spec in sorted(servers.items()):
|
||||
if not isinstance(spec, dict):
|
||||
print(f" {name}: invalid entry")
|
||||
continue
|
||||
command = spec.get("command", "?")
|
||||
args = spec.get("args", [])
|
||||
server_type = spec.get("type", "")
|
||||
suffix = f" type={server_type}" if server_type else ""
|
||||
print(f" {name}: {command} {args}{suffix}")
|
||||
if spec.get("env"):
|
||||
print(" env: <redacted>")
|
||||
PY
|
||||
|
||||
section "Claude Code hooks"
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
path = Path.home() / ".claude/settings.json"
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except FileNotFoundError:
|
||||
print("MISSING")
|
||||
raise SystemExit(0)
|
||||
except Exception as exc:
|
||||
print(f"INVALID: {exc}")
|
||||
raise SystemExit(0)
|
||||
|
||||
hooks = data.get("hooks", {})
|
||||
if not isinstance(hooks, dict) or not hooks:
|
||||
print("no hooks")
|
||||
raise SystemExit(0)
|
||||
for event, entries in hooks.items():
|
||||
print(event)
|
||||
if not isinstance(entries, list):
|
||||
print(" invalid entries")
|
||||
continue
|
||||
for entry in entries:
|
||||
matcher = entry.get("matcher", "*") if isinstance(entry, dict) else "?"
|
||||
commands = entry.get("hooks", []) if isinstance(entry, dict) else []
|
||||
rendered = []
|
||||
for command in commands if isinstance(commands, list) else []:
|
||||
if isinstance(command, dict):
|
||||
rendered.append(" ".join([str(command.get("command", "?")), *map(str, command.get("args", []))]))
|
||||
print(f" matcher={matcher}: {', '.join(rendered) or 'no command'}")
|
||||
PY
|
||||
|
||||
section "Headroom"
|
||||
if bashio::config.true 'install_headroom'; then
|
||||
curl -fsS --max-time 2 http://127.0.0.1:8787/health && echo || echo "proxy health: FAILED"
|
||||
headroom mcp status || true
|
||||
headroom savings || true
|
||||
else
|
||||
echo "disabled"
|
||||
fi
|
||||
|
||||
section "RTK"
|
||||
if bashio::config.true 'install_rtk'; then
|
||||
rtk gain || true
|
||||
else
|
||||
echo "disabled"
|
||||
fi
|
||||
|
||||
section "TokenSave"
|
||||
if bashio::config.true 'install_tokensave'; then
|
||||
tokensave doctor --agent claude || true
|
||||
tokensave gain --all --range 30d || true
|
||||
while IFS= read -r configured_path || [ -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 < <(bashio::config '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
|
||||
236
claude_desktop/rootfs/usr/local/bin/ha-cli
Executable file
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ha-cli — talk to the local Home Assistant Core API from inside the add-on.
|
||||
|
||||
Lets Claude Code configure Home Assistant (automations, scripts, scenes,
|
||||
helpers, dashboards, areas/labels, service calls) through the API, without any
|
||||
`/config` filesystem mount. `secrets.yaml` and other add-ons' credentials are
|
||||
therefore never reachable.
|
||||
|
||||
Authentication and the base URL are resolved automatically, in this order:
|
||||
|
||||
1. $HA_BASE_URL + $HA_TOKEN explicit override (advanced/scoped)
|
||||
2. `ha_mcp_token` in /data/options.json scoped user long-lived token -> :8123
|
||||
3. $SUPERVISOR_TOKEN Supervisor Core-API proxy fallback
|
||||
(admin-equivalent; needs
|
||||
homeassistant_api: true, which this
|
||||
add-on sets — zero setup)
|
||||
|
||||
The scoped token is checked before the Supervisor fallback so setting
|
||||
`ha_mcp_token` actually narrows access instead of being shadowed by the
|
||||
always-present admin-equivalent Supervisor token.
|
||||
|
||||
Subcommands:
|
||||
ha-cli get <path> GET e.g. get config/automation/config/1700000000
|
||||
ha-cli post <path> [BODY] POST BODY = inline JSON, @file, or - (stdin)
|
||||
ha-cli delete <path> DELETE
|
||||
ha-cli call <domain.service> [BODY] call a service (BODY = JSON service data)
|
||||
ha-cli states [entity_id] all states, or one entity
|
||||
ha-cli config GET /config (sanity check / core info)
|
||||
ha-cli ws <BODY> one WebSocket command (BODY = JSON, @file, or -)
|
||||
|
||||
`<path>` is relative to the REST API root; a leading slash and/or `api/` prefix
|
||||
are optional. Responses are printed as formatted JSON. Exit code is non-zero on
|
||||
HTTP or API errors.
|
||||
|
||||
Use the WebSocket subcommand for things the REST API does not expose:
|
||||
ha-cli ws '{"type":"config/area_registry/list"}'
|
||||
ha-cli ws '{"type":"input_boolean/create","name":"Guest mode","icon":"mdi:account"}'
|
||||
ha-cli ws '{"type":"lovelace/config","url_path":null}'
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
def _load_option(name):
|
||||
"""Read a single option from the add-on's /data/options.json, if present."""
|
||||
try:
|
||||
with open("/data/options.json", encoding="utf-8") as handle:
|
||||
return json.load(handle).get(name)
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _helper_enabled():
|
||||
"""Mirror config.yaml's enable_ha_api_helper default (true) when unset."""
|
||||
value = _load_option("enable_ha_api_helper")
|
||||
return value is not False
|
||||
|
||||
|
||||
def resolve_endpoint():
|
||||
"""Return (rest_base, ws_url, token) for the best available auth path."""
|
||||
base = os.environ.get("HA_BASE_URL")
|
||||
token = os.environ.get("HA_TOKEN")
|
||||
if base and token:
|
||||
rest = base.rstrip("/")
|
||||
if not rest.endswith("/api"):
|
||||
rest += "/api"
|
||||
ws = rest.replace("http", "ws", 1).rsplit("/api", 1)[0] + "/api/websocket"
|
||||
return rest, ws, token
|
||||
|
||||
# Checked before SUPERVISOR_TOKEN: this add-on always sets homeassistant_api,
|
||||
# so the admin-equivalent Supervisor token is otherwise always present and
|
||||
# would shadow a user's deliberately scoped-down ha_mcp_token.
|
||||
token = _load_option("ha_mcp_token")
|
||||
if token:
|
||||
return (
|
||||
"http://homeassistant:8123/api",
|
||||
"ws://homeassistant:8123/api/websocket",
|
||||
token,
|
||||
)
|
||||
|
||||
token = os.environ.get("SUPERVISOR_TOKEN")
|
||||
if token:
|
||||
return "http://supervisor/core/api", "ws://supervisor/core/websocket", token
|
||||
|
||||
sys.exit(
|
||||
"ha-cli: no credentials. Expected ha_mcp_token in the add-on options "
|
||||
"(scoped user), $SUPERVISOR_TOKEN (admin-equivalent fallback, default "
|
||||
"inside the add-on), or an explicit $HA_BASE_URL+$HA_TOKEN override."
|
||||
)
|
||||
|
||||
|
||||
def _url(base, path):
|
||||
path = path.lstrip("/")
|
||||
if path.startswith("api/"):
|
||||
path = path[len("api/"):]
|
||||
return base.rstrip("/") + "/" + path
|
||||
|
||||
|
||||
def _read_body(arg):
|
||||
"""Resolve an inline-JSON / @file / - (stdin) body argument to a dict/list."""
|
||||
if arg is None:
|
||||
return None
|
||||
if arg == "-":
|
||||
raw = sys.stdin.read()
|
||||
elif arg.startswith("@"):
|
||||
with open(arg[1:], encoding="utf-8") as handle:
|
||||
raw = handle.read()
|
||||
else:
|
||||
raw = arg
|
||||
raw = raw.strip()
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except ValueError as exc:
|
||||
sys.exit(f"ha-cli: body is not valid JSON: {exc}")
|
||||
|
||||
|
||||
def _print(obj):
|
||||
if isinstance(obj, (dict, list)):
|
||||
print(json.dumps(obj, indent=2, ensure_ascii=False))
|
||||
elif obj not in (None, ""):
|
||||
print(obj)
|
||||
|
||||
|
||||
def rest(method, base, token, path, body=None):
|
||||
data = None
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
if body is not None:
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(_url(base, path), data=data, method=method, headers=headers)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
text = resp.read().decode("utf-8")
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", "replace").strip()
|
||||
sys.exit(f"ha-cli: HTTP {exc.code} {exc.reason} on {method} {path}\n{detail}")
|
||||
except urllib.error.URLError as exc:
|
||||
sys.exit(f"ha-cli: cannot reach Home Assistant ({exc.reason}) on {method} {path}")
|
||||
try:
|
||||
return json.loads(text) if text.strip() else None
|
||||
except ValueError:
|
||||
return text
|
||||
|
||||
|
||||
def ws_command(ws_url, token, command):
|
||||
try:
|
||||
import asyncio
|
||||
|
||||
import websockets
|
||||
except ImportError:
|
||||
sys.exit(
|
||||
"ha-cli: the 'websockets' Python package is required for the ws "
|
||||
"subcommand. REST subcommands work without it."
|
||||
)
|
||||
|
||||
async def run():
|
||||
async with websockets.connect(ws_url, max_size=None) as sock:
|
||||
hello = json.loads(await sock.recv())
|
||||
if hello.get("type") != "auth_required":
|
||||
raise SystemExit(f"ha-cli: unexpected WS greeting: {hello}")
|
||||
await sock.send(json.dumps({"type": "auth", "access_token": token}))
|
||||
if json.loads(await sock.recv()).get("type") != "auth_ok":
|
||||
raise SystemExit("ha-cli: WebSocket authentication failed")
|
||||
payload = dict(command)
|
||||
payload["id"] = 1
|
||||
await sock.send(json.dumps(payload))
|
||||
while True:
|
||||
msg = json.loads(await sock.recv())
|
||||
if msg.get("id") == 1 and msg.get("type") == "result":
|
||||
return msg
|
||||
|
||||
result = asyncio.run(run())
|
||||
if not result.get("success", True):
|
||||
_print(result.get("error", result))
|
||||
sys.exit(1)
|
||||
return result.get("result", result)
|
||||
|
||||
|
||||
def main(argv):
|
||||
if not argv or argv[0] in ("-h", "--help", "help"):
|
||||
print(__doc__)
|
||||
return 0
|
||||
|
||||
if not _helper_enabled():
|
||||
sys.exit(
|
||||
"ha-cli: disabled (enable_ha_api_helper is false in the add-on "
|
||||
"options). Enable it there to let Claude configure Home Assistant."
|
||||
)
|
||||
|
||||
rest_base, ws_url, token = resolve_endpoint()
|
||||
cmd, args = argv[0], argv[1:]
|
||||
|
||||
if cmd == "get":
|
||||
if len(args) != 1:
|
||||
sys.exit("usage: ha-cli get <path>")
|
||||
_print(rest("GET", rest_base, token, args[0]))
|
||||
elif cmd == "post":
|
||||
if not args:
|
||||
sys.exit("usage: ha-cli post <path> [BODY]")
|
||||
body = _read_body(args[1]) if len(args) > 1 else None
|
||||
_print(rest("POST", rest_base, token, args[0], body))
|
||||
elif cmd == "delete":
|
||||
if len(args) != 1:
|
||||
sys.exit("usage: ha-cli delete <path>")
|
||||
_print(rest("DELETE", rest_base, token, args[0]))
|
||||
elif cmd == "call":
|
||||
if not args or "." not in args[0]:
|
||||
sys.exit("usage: ha-cli call <domain.service> [BODY]")
|
||||
domain, service = args[0].split(".", 1)
|
||||
body = _read_body(args[1]) if len(args) > 1 else None
|
||||
_print(rest("POST", rest_base, token, f"services/{domain}/{service}", body or {}))
|
||||
elif cmd == "states":
|
||||
path = f"states/{args[0]}" if args else "states"
|
||||
_print(rest("GET", rest_base, token, path))
|
||||
elif cmd == "config":
|
||||
_print(rest("GET", rest_base, token, "config"))
|
||||
elif cmd == "ws":
|
||||
if len(args) != 1:
|
||||
sys.exit("usage: ha-cli ws <BODY>")
|
||||
command = _read_body(args[0])
|
||||
if not isinstance(command, dict) or "type" not in command:
|
||||
sys.exit('ha-cli: ws BODY must be a JSON object with a "type" field')
|
||||
_print(ws_command(ws_url, token, command))
|
||||
else:
|
||||
sys.exit(f"ha-cli: unknown subcommand '{cmd}' (try: ha-cli --help)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
218
claude_desktop/rootfs/usr/local/bin/headroom-posttooluse-compress.py
Executable file
@@ -0,0 +1,218 @@
|
||||
#!/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)
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -3,5 +3,7 @@
|
||||
"upstream_repo": "linuxserver/docker-baseimage-selkies",
|
||||
"github_fulltag": true,
|
||||
"slug": "claude_desktop",
|
||||
"paused": false
|
||||
"paused": false,
|
||||
"upstream_version": "ubunturesolute-version-6dc44b0e",
|
||||
"last_update": "2026-07-13"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
|
||||
## 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)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
#=== Home Assistant Addon ===#
|
||||
|
||||
# ARGs used in FROM must be declared before any FROM instruction
|
||||
ARG BUILD_UPSTREAM="2.9.14"
|
||||
ARG BUILD_UPSTREAM="2.9.16"
|
||||
|
||||
#################
|
||||
# 1 Build Image #
|
||||
|
||||
@@ -91,5 +91,5 @@ schema:
|
||||
TZ: str?
|
||||
slug: cleanuparr
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/cleanuparr
|
||||
version: "2.9.14"
|
||||
version: "2.9.16"
|
||||
webui: "[PROTO:ssl]://[HOST]:[PORT:11011]"
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"last_update": "2026-06-20",
|
||||
"last_update": "2026-07-11",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "cleanuparr",
|
||||
"source": "github",
|
||||
"upstream_repo": "Cleanuparr/Cleanuparr",
|
||||
"upstream_version": "2.9.14"
|
||||
"upstream_version": "2.9.16"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,4 +1,7 @@
|
||||
|
||||
## 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)
|
||||
|
||||
|
||||
@@ -101,4 +101,4 @@ schema:
|
||||
slug: codex
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "2.1.0"
|
||||
version: "2.1.2"
|
||||
|
||||
BIN
codex/stats.png
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"github_beta": "true",
|
||||
"last_update": "2026-07-04",
|
||||
"last_update": "2026-07-11",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "codex",
|
||||
"source": "github",
|
||||
"upstream_repo": "ajslater/codex",
|
||||
"upstream_version": "2.1.0"
|
||||
"upstream_version": "2.1.2"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +1,24 @@
|
||||
## 8.19.18-3 (14-07-2026)
|
||||
- Force a fresh image pull for users left on a stale cached image (some upgrades kept running the old Elasticsearch 7.17.9 image, failing with `mv: cannot move '/data/config' ... Permission denied` and `AccessDeniedException[/usr/share/elasticsearch/data/nodes/0]`). Fully stop and update the add-on so Home Assistant pulls this build.
|
||||
- Replaced the cryptic `Permission denied` failure with a clear message when the add-on is not running as root (the state that caused the failure above).
|
||||
|
||||
## 8.19.18-2 (14-07-2026)
|
||||
- Minor bugs fixed
|
||||
## 8.19.18 (2026-07-14)
|
||||
|
||||
- Upgrade to Elasticsearch 8.19.18 (#2849). Note: despite the previous add-on version reading `8.14.3`, the shipped image was still Elasticsearch 7.17.9 — the Dockerfile upstream version was never bumped. This release actually delivers 8.x, making the add-on compatible with the `homeassistant-elasticsearch` integration (requires 8.14+).
|
||||
- Automatic data migration: existing 7.17 data is upgraded in place by Elasticsearch on first start (one-way; can take a while on large datasets). A migration guard aborts with a clear message on unsupported paths (downgrades, or data more than one major version old). Take a Home Assistant backup before updating.
|
||||
- The previous bundled config directory is archived to `/data/config.bak-<old-version>` during major upgrades; re-apply custom settings to the new config if needed.
|
||||
- Security (`xpack.security.enabled`) defaults to `false` to preserve the previous plain-HTTP behavior. Override by adding `ES_SETTING_XPACK_SECURITY_ENABLED` (or any `ES_SETTING_XPACK_SECURITY_*` variable) in the add-on's `env_vars` option.
|
||||
- Fixed the `env_vars` add-on option, which previously had no effect: variables are now exported before Elasticsearch starts.
|
||||
- Removed the `ingest-attachment` plugin install: it is a bundled module since Elasticsearch 8.0.
|
||||
- Startup persistence logic rewritten as a proper init script (`/usr/local/bin/addon-init.sh`) instead of line-number-based entrypoint patching.
|
||||
- Added `updater.json` so upstream 8.19.x releases are tracked automatically (pinned to the 8.19 line: 9.x cannot read indices created in 7.x).
|
||||
- The upstream 8.x image ends the build as a non-root user with a read-only entrypoint; the Dockerfile now switches to root for the build steps that patch/install into it. The container also starts as root (unchanged from 7.17.9) so `addon-init.sh` can chown/move pre-existing `/data` content that may be owned by root from earlier installs; unlike 7.17.9's own entrypoint, the upstream 8.x entrypoint no longer drops privileges before starting Elasticsearch (which refuses to run as root), so `addon-init.sh` now does that itself via `chroot --userspec=1000:0` once its root-only work is done.
|
||||
- `env_vars` names starting with a digit are now rejected before export instead of crashing the entrypoint.
|
||||
- Fixed a startup failure (`mv: cannot move '/data/config' ... Permission denied`) on upgrade from an existing 7.17.9 install, caused by an earlier fix in this same release that switched the runtime user to non-root before this fix was in place.
|
||||
- Fixed a second regression from that same fix: without a privilege drop before starting Elasticsearch, both fresh installs and upgrades would fail Elasticsearch's own root-check ("can not run elasticsearch as root").
|
||||
|
||||
## 8.14.3-3 (2026-06-19)
|
||||
- Fix startup failing with `chroot: cannot change root directory` by allowing `capability sys_chroot` in the AppArmor profile (#2709)
|
||||
- Fix AppArmor profile name (was `inadyn_addon`, colliding with several other add-ons); renamed to `elasticsearch_addon`
|
||||
|
||||
@@ -14,9 +14,14 @@
|
||||
# 1 Build Image #
|
||||
#################
|
||||
|
||||
ARG BUILD_UPSTREAM="7.17.9"
|
||||
ARG BUILD_UPSTREAM="8.19.18"
|
||||
FROM elasticsearch:$BUILD_UPSTREAM
|
||||
|
||||
# The base image ends as USER 1000:0 with a root-owned, read-only (0555)
|
||||
# entrypoint; switch back to root for the remaining build steps (entrypoint
|
||||
# patch, package install, chmod), then restore the Elasticsearch user below
|
||||
USER root
|
||||
|
||||
##################
|
||||
# 2 Modify Image #
|
||||
##################
|
||||
@@ -26,20 +31,15 @@ ENV S6_CMD_WAIT_FOR_SERVICES=1 \
|
||||
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
|
||||
S6_SERVICES_GRACETIME=0
|
||||
|
||||
# Expose the upstream version to the add-on init script (migration guard)
|
||||
ARG BUILD_UPSTREAM
|
||||
ENV UPSTREAM_VERSION="$BUILD_UPSTREAM"
|
||||
|
||||
# Data persistence
|
||||
# hadolint ignore=SC2016
|
||||
RUN sed -i '5a echo "Data location moved. Please wait while elasticsearch starts..."' /usr/local/bin/docker-entrypoint.sh \
|
||||
&& sed -i '5a chown -R $(id -u):$(id -g) $HOME' /usr/local/bin/docker-entrypoint.sh \
|
||||
&& sed -i '5a done' /usr/local/bin/docker-entrypoint.sh \
|
||||
&& sed -i '5a ln -s $NEWHOME/$file /usr/share/elasticsearch || true' /usr/local/bin/docker-entrypoint.sh \
|
||||
&& sed -i '5a rm -r /usr/share/elasticsearch/$file || true' /usr/local/bin/docker-entrypoint.sh \
|
||||
&& sed -i '5a cp -rn /usr/share/elasticsearch/$file $NEWHOME || true' /usr/local/bin/docker-entrypoint.sh \
|
||||
&& sed -i '5a for file in "data" "config"; do' /usr/local/bin/docker-entrypoint.sh \
|
||||
&& sed -i '5a mkdir -p $NEWHOME' /usr/local/bin/docker-entrypoint.sh \
|
||||
&& sed -i '5a NEWHOME="/data"' /usr/local/bin/docker-entrypoint.sh \
|
||||
# Install plugins
|
||||
&& /usr/share/elasticsearch/bin/elasticsearch-plugin install --batch ingest-attachment
|
||||
# Data persistence & migration: source the add-on init script at the top of
|
||||
# the official entrypoint (pattern-anchored; ingest-attachment is bundled
|
||||
# since ES 8.0 so no plugin install is needed anymore)
|
||||
RUN sed -i '/^set -e$/a . /usr/local/bin/addon-init.sh' /usr/local/bin/docker-entrypoint.sh \
|
||||
&& grep -q "addon-init.sh" /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
##################
|
||||
# 3 Install apps #
|
||||
@@ -62,7 +62,7 @@ COPY ha_automodules.sh /ha_automodules.sh
|
||||
RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_automodules.sh
|
||||
|
||||
# Manual apps
|
||||
ENV PACKAGES=""
|
||||
ENV PACKAGES="jq"
|
||||
|
||||
# Automatic apps & bashio
|
||||
COPY ha_autoapps.sh /ha_autoapps.sh
|
||||
@@ -140,3 +140,11 @@ HEALTHCHECK \
|
||||
--start-period=30s \
|
||||
--timeout=25s \
|
||||
CMD curl -A "HealthCheck: Docker/1.0" -s -f "http://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" &>/dev/null || exit 1
|
||||
|
||||
# Start as root: addon-init.sh needs it to chown/move pre-existing /data
|
||||
# content that may be owned by root from earlier installs. It drops to
|
||||
# uid 1000 itself (via chroot --userspec) before Elasticsearch actually
|
||||
# starts, since Elasticsearch refuses to run as root and the upstream 8.x
|
||||
# entrypoint no longer does that drop on its own (7.x's did). This matches
|
||||
# the addon's own AppArmor profile (chown, setuid, setgid, sys_chroot,
|
||||
# mount capabilities).
|
||||
|
||||
@@ -91,6 +91,22 @@ Connect other applications to Elasticsearch using:
|
||||
|
||||
Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
|
||||
|
||||
Elasticsearch settings can be set through variables named `ES_SETTING_<SETTING_WITH_UNDERSCORES>`; for example `ES_SETTING_XPACK_SECURITY_ENABLED` maps to `xpack.security.enabled`.
|
||||
|
||||
### Security
|
||||
|
||||
To preserve the plain-HTTP behavior of previous versions (and compatibility with the Home Assistant Elasticsearch integration), `xpack.security.enabled` defaults to `false`. To enable Elasticsearch security, add `ES_SETTING_XPACK_SECURITY_ENABLED` with value `true` in `env_vars`.
|
||||
|
||||
## Upgrading from 7.x
|
||||
|
||||
The upgrade to Elasticsearch 8.x is automatic and **one-way**:
|
||||
|
||||
1. Take a Home Assistant backup of the add-on before updating.
|
||||
2. Update the add-on and start it. Elasticsearch upgrades the existing indices in place on first start — this can take a while on large datasets; do **not** stop the add-on during the first start.
|
||||
3. The previous bundled config directory is archived to `/data/config.bak-<old-version>`; re-apply any custom settings to the new config.
|
||||
|
||||
Downgrading afterwards is not supported by Elasticsearch — restore the backup instead.
|
||||
|
||||
## Integration with HA
|
||||
|
||||
Component : https://community.home-assistant.io/t/elasticsearch-component-publish-home-assistant-events-to-elasticsearch/66877
|
||||
|
||||
@@ -90,4 +90,4 @@ slug: elasticsearch
|
||||
startup: services
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/elasticsearch
|
||||
version: 8.14.3-3
|
||||
version: 8.19.18-3
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
#!/bin/bash
|
||||
182
elasticsearch/rootfs/usr/local/bin/addon-init.sh
Executable file
@@ -0,0 +1,182 @@
|
||||
#!/bin/bash
|
||||
# shellcheck shell=bash
|
||||
# Sourced by /usr/local/bin/docker-entrypoint.sh (right after "set -e"),
|
||||
# before Elasticsearch starts. The container starts as root (see
|
||||
# Dockerfile) so this script can chown/move pre-existing /data content
|
||||
# that may be owned by root from earlier installs. Elasticsearch itself
|
||||
# refuses to run as root, and unlike 7.x the upstream 8.x entrypoint no
|
||||
# longer drops privileges on its own, so this script does it at the end
|
||||
# (section 6) by re-execing the entrypoint as uid 1000. On that re-exec'd
|
||||
# pass this script just returns immediately (see the guard right below).
|
||||
#
|
||||
# Responsibilities:
|
||||
# 1. Export user env_vars from /data/options.json
|
||||
# 2. Default xpack.security.enabled=false (7.x behavior) unless user overrides
|
||||
# 3. Relocate data & config to /data for persistence (idempotent)
|
||||
# 4. Guard major-version data migrations (7.x -> 8.x is automatic)
|
||||
# 5. Record the running version once Elasticsearch is confirmed healthy
|
||||
# 6. Drop root privileges before Elasticsearch actually starts
|
||||
|
||||
if [ -n "${_ADDON_INIT_REEXEC:-}" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "-----------------------------------------------------------"
|
||||
echo " Add-on: Elasticsearch server"
|
||||
echo " Upstream version: ${UPSTREAM_VERSION:-unknown}"
|
||||
echo "-----------------------------------------------------------"
|
||||
|
||||
ES_HOME="/usr/share/elasticsearch"
|
||||
PERSISTENT_HOME="/data"
|
||||
VERSION_MARKER="$PERSISTENT_HOME/.addon-upstream-version"
|
||||
OPTIONS_JSON="/data/options.json"
|
||||
|
||||
# This first pass must be root so it can relocate and take ownership of
|
||||
# pre-existing /data content written by an earlier (root) install. If it
|
||||
# is not root (e.g. an old cached image that pinned USER 1000:0, or the
|
||||
# container being forced to another user), the moves/chowns below fail
|
||||
# with a cryptic "Permission denied"; fail loudly with the real reason.
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "FATAL: the Elasticsearch add-on must start as root (currently uid $(id -u))."
|
||||
echo "If you upgraded from an older version, the running image is likely stale - fully stop and update/reinstall the add-on so Home Assistant pulls the current image."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
############################
|
||||
# 1 Export user env_vars #
|
||||
############################
|
||||
|
||||
if [ -f "$OPTIONS_JSON" ] && command -v jq >/dev/null 2>&1; then
|
||||
while IFS= read -r pair; do
|
||||
name=$(jq -r '.name // empty' <<<"$pair")
|
||||
value=$(jq -r '.value // empty' <<<"$pair")
|
||||
if [[ $name =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
|
||||
echo "Setting env variable from options: $name"
|
||||
export "$name"="$value"
|
||||
elif [ -n "$name" ]; then
|
||||
echo "WARNING: ignoring invalid env_vars name: $name"
|
||||
fi
|
||||
done < <(jq -c '.env_vars[]?' "$OPTIONS_JSON" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
##################################
|
||||
# 2 Security default (7.x parity)#
|
||||
##################################
|
||||
|
||||
# ES 8+ enables security + TLS by default, which breaks plain-http clients
|
||||
# such as the homeassistant-elasticsearch component. Keep the previous 7.x
|
||||
# behavior unless the user explicitly configures xpack.security themselves
|
||||
# (either as a dotted setting or via the ES_SETTING_* translation).
|
||||
if ! env | grep -qiE '^(xpack\.security\.|ES_SETTING_XPACK_SECURITY_)'; then
|
||||
export ES_SETTING_XPACK_SECURITY_ENABLED=false
|
||||
echo "Security: xpack.security.enabled=false (default; override by setting ES_SETTING_XPACK_SECURITY_ENABLED in env_vars)"
|
||||
fi
|
||||
|
||||
############################
|
||||
# 3 Migration guard #
|
||||
############################
|
||||
|
||||
current_version="${UPSTREAM_VERSION:-0.0.0}"
|
||||
current_major="${current_version%%.*}"
|
||||
data_version=""
|
||||
|
||||
if [ -f "$VERSION_MARKER" ]; then
|
||||
data_version="$(head -n 1 "$VERSION_MARKER" | tr -cd '0-9.')"
|
||||
elif [ -d "$PERSISTENT_HOME/data" ] && [ -n "$(ls -A "$PERSISTENT_HOME/data" 2>/dev/null)" ]; then
|
||||
# Existing data without a marker: only 7.17.9 was ever shipped before markers
|
||||
data_version="7.17.9"
|
||||
fi
|
||||
|
||||
if [ -n "$data_version" ] && [[ $current_major =~ ^[0-9]+$ ]]; then
|
||||
data_major="${data_version%%.*}"
|
||||
if [ "$data_major" -gt "$current_major" ]; then
|
||||
echo "FATAL: existing data was written by Elasticsearch $data_version but this add-on runs $current_version."
|
||||
echo "Downgrading Elasticsearch data is not supported. Restore a Home Assistant snapshot taken with the newer version, or delete the add-on data to start fresh."
|
||||
exit 1
|
||||
elif [ "$((current_major - data_major))" -gt 1 ]; then
|
||||
echo "FATAL: existing data was written by Elasticsearch $data_version, which is more than one major version behind $current_version."
|
||||
echo "Elasticsearch can only upgrade data from the previous major version. Upgrade stepwise (e.g. $data_major.x -> $((data_major + 1)).x -> ...) or delete the add-on data to start fresh."
|
||||
exit 1
|
||||
elif [ "$data_major" -lt "$current_major" ]; then
|
||||
echo "NOTICE: one-time automatic data migration from Elasticsearch $data_version to $current_version."
|
||||
echo "NOTICE: indices are upgraded automatically on startup. This can take a while on large datasets - do NOT stop the add-on during the first start."
|
||||
# The bundled config from the old major is stale (jvm.options, log4j2,
|
||||
# security settings). Archive it so a fresh one is seeded below.
|
||||
if [ -d "$PERSISTENT_HOME/config" ] && [ ! -L "$PERSISTENT_HOME/config" ]; then
|
||||
config_backup="$PERSISTENT_HOME/config.bak-$data_version"
|
||||
if [ ! -e "$config_backup" ]; then
|
||||
if ! mv "$PERSISTENT_HOME/config" "$config_backup"; then
|
||||
echo "FATAL: could not archive the old config to $config_backup."
|
||||
echo "This add-on must run as root to migrate a previous install. Restore a Home Assistant backup and ensure the add-on is not forced to a non-root user."
|
||||
exit 1
|
||||
fi
|
||||
echo "NOTICE: previous config archived to $config_backup. Re-apply any custom settings to the new config."
|
||||
fi
|
||||
fi
|
||||
# The container config dir may still symlink to the archived config
|
||||
if [ -L "$ES_HOME/config" ]; then
|
||||
rm -f "$ES_HOME/config"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
############################
|
||||
# 4 Data persistence #
|
||||
############################
|
||||
|
||||
mkdir -p "$PERSISTENT_HOME"
|
||||
for dir in "data" "config"; do
|
||||
if [ ! -L "$ES_HOME/$dir" ]; then
|
||||
if [ -d "$ES_HOME/$dir" ]; then
|
||||
cp -rn "$ES_HOME/$dir" "$PERSISTENT_HOME" 2>/dev/null || true
|
||||
rm -rf "${ES_HOME:?}/$dir"
|
||||
fi
|
||||
mkdir -p "$PERSISTENT_HOME/$dir"
|
||||
ln -s "$PERSISTENT_HOME/$dir" "$ES_HOME/$dir"
|
||||
fi
|
||||
done
|
||||
|
||||
# Make the persisted files usable by the elasticsearch user (uid 1000),
|
||||
# which the official entrypoint drops to when started as root
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
chown -R 1000:0 "$PERSISTENT_HOME/data" "$PERSISTENT_HOME/config" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "Data location: $PERSISTENT_HOME (persistent). Please wait while elasticsearch starts..."
|
||||
|
||||
############################
|
||||
# 5 Record data version #
|
||||
############################
|
||||
|
||||
# Only record the running version once ES is confirmed healthy, so a failed
|
||||
# upgrade attempt never masks the true on-disk data lineage
|
||||
if [ "$data_version" != "$current_version" ]; then
|
||||
(
|
||||
for _ in $(seq 1 180); do
|
||||
# Check the HTTP status directly instead of curl -f: a 401 means
|
||||
# Elasticsearch is up and answering (security just requires
|
||||
# auth), so it must count as healthy too, not as a failure.
|
||||
status=$(curl -A "HealthCheck: Docker/1.0" -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:9200" 2>/dev/null || true)
|
||||
if [ "$status" = "200" ] || [ "$status" = "401" ]; then
|
||||
echo "$current_version" >"$VERSION_MARKER"
|
||||
echo "Elasticsearch $current_version started successfully; data version recorded."
|
||||
exit 0
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
) &
|
||||
fi
|
||||
|
||||
############################
|
||||
# 6 Drop privileges #
|
||||
############################
|
||||
|
||||
# Elasticsearch refuses to start as root ("can not run elasticsearch as
|
||||
# root"). 7.x's own entrypoint dropped to uid 1000 via chroot before
|
||||
# launching Elasticsearch; 8.x no longer does that, so do it here instead,
|
||||
# then let the entrypoint continue as uid 1000 (matches the sys_chroot /
|
||||
# setuid / setgid capabilities already granted in the AppArmor profile).
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
export _ADDON_INIT_REEXEC=1
|
||||
exec chroot --userspec=1000:0 / /usr/local/bin/docker-entrypoint.sh "$@"
|
||||
fi
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.7 KiB |