Compare commits
338 Commits
agent/remo
...
bcc521015b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcc521015b | ||
|
|
6b46611a0b | ||
|
|
a72e03e88a | ||
|
|
8042ac6786 | ||
|
|
5b7de5907f | ||
|
|
0d344ecd6c | ||
|
|
f34848ae42 | ||
|
|
61627e80bb | ||
|
|
6f693f39dc | ||
|
|
0397e2b849 | ||
|
|
865e1c8e6d | ||
|
|
25945f5a37 | ||
|
|
feaf4f5c92 | ||
|
|
fa83755108 | ||
|
|
101c252b71 | ||
|
|
1f177e26f4 | ||
|
|
9232156924 | ||
|
|
766abd5ec3 | ||
|
|
49fbbbf59c | ||
|
|
a55c410b9c | ||
|
|
e32a134ee2 | ||
|
|
0019b52f4a | ||
|
|
a240dd8d00 | ||
|
|
408f9ee05b | ||
|
|
324dc560c2 | ||
|
|
cc8b402942 | ||
|
|
8eb51526a1 | ||
|
|
4c068c6792 | ||
|
|
476f02924d | ||
|
|
2964b762d1 | ||
|
|
b942bcf336 | ||
|
|
3973e5901f | ||
|
|
1b313e591d | ||
|
|
49130bafc8 | ||
|
|
6463cc964a | ||
|
|
3d2773db75 | ||
|
|
6ad59a6e54 | ||
|
|
8ad03ce68c | ||
|
|
11de95228d | ||
|
|
d6d09a0204 | ||
|
|
cd98281978 | ||
|
|
f35f0baaeb | ||
|
|
5dcb1c222a | ||
|
|
1c981e187f | ||
|
|
3725f920e0 | ||
|
|
e21496f18d | ||
|
|
4dd0e22a84 | ||
|
|
fe60b6d6b1 | ||
|
|
f4877df9cc | ||
|
|
8c1152501d | ||
|
|
ac72fac79c | ||
|
|
bb51a80700 | ||
|
|
1a50bbe522 | ||
|
|
2fa921734b | ||
|
|
2bdf65187c | ||
|
|
881821e17c | ||
|
|
63fcca6f74 | ||
|
|
dcf8073734 | ||
|
|
6ad65abc8e | ||
|
|
8ee044b697 | ||
|
|
1b32956a2a | ||
|
|
36e0800596 | ||
|
|
c7bc78f306 | ||
|
|
090b200cb1 | ||
|
|
c3e81e8761 | ||
|
|
f85151b52c | ||
|
|
179863a7b0 | ||
|
|
8d67dea5f1 | ||
|
|
4a05b9a287 | ||
|
|
acee58b78d | ||
|
|
d9e08a447b | ||
|
|
49dcc057d5 | ||
|
|
a6d93753e9 | ||
|
|
920591631d | ||
|
|
15853bd661 | ||
|
|
cf918a2c2b | ||
|
|
680a947ced | ||
|
|
b6fc7f2c3a | ||
|
|
9add537dc6 | ||
|
|
99ccd61c14 | ||
|
|
dc5c085598 | ||
|
|
778ef33359 | ||
|
|
4804041920 | ||
|
|
510bd46128 | ||
|
|
7a375df1de | ||
|
|
295c3d912f | ||
|
|
f4cfc359fa | ||
|
|
0b61865f36 | ||
|
|
8c763216c8 | ||
|
|
dd885ceaee | ||
|
|
51288da9d0 | ||
|
|
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,
|
DDanii,
|
||||||
DMurzNN,
|
DMurzNN,
|
||||||
DUC750,
|
DUC750,
|
||||||
|
DY-hub,
|
||||||
DaFlowah,
|
DaFlowah,
|
||||||
DaJonas94,
|
DaJonas94,
|
||||||
Daafip,Netherlands
|
Daafip,Netherlands
|
||||||
@@ -238,6 +239,7 @@ EtienneMD,
|
|||||||
Evel270,
|
Evel270,
|
||||||
Everestlion,
|
Everestlion,
|
||||||
EvertJob,
|
EvertJob,
|
||||||
|
Exlatis,
|
||||||
Extrunder,
|
Extrunder,
|
||||||
F0264,
|
F0264,
|
||||||
F4bsi,Germany
|
F4bsi,Germany
|
||||||
@@ -563,6 +565,7 @@ PhoenixTwoFive,Germany
|
|||||||
PhysShell,
|
PhysShell,
|
||||||
PierreNa,France
|
PierreNa,France
|
||||||
PietroSpina,
|
PietroSpina,
|
||||||
|
Pingmin,
|
||||||
PiotrKrzyzek,United States
|
PiotrKrzyzek,United States
|
||||||
PiratesGhost,
|
PiratesGhost,
|
||||||
Pixelzeus,
|
Pixelzeus,
|
||||||
@@ -897,10 +900,12 @@ antorimba,
|
|||||||
antx-code,
|
antx-code,
|
||||||
anyezhe,
|
anyezhe,
|
||||||
aorosora,
|
aorosora,
|
||||||
|
araminimichael,
|
||||||
arbal,United States
|
arbal,United States
|
||||||
ardemk,
|
ardemk,
|
||||||
ared469,
|
ared469,
|
||||||
arethefreshest,
|
arethefreshest,
|
||||||
|
arozoire,
|
||||||
arpit-mehra,
|
arpit-mehra,
|
||||||
artemave,France
|
artemave,France
|
||||||
artemdanielov,
|
artemdanielov,
|
||||||
@@ -1156,6 +1161,7 @@ danbruno,
|
|||||||
danctrl,Germany
|
danctrl,Germany
|
||||||
danez,United States
|
danez,United States
|
||||||
danieldotnl,Netherlands
|
danieldotnl,Netherlands
|
||||||
|
danishru,
|
||||||
dannybeeckman,
|
dannybeeckman,
|
||||||
dannybloomfield,United States
|
dannybloomfield,United States
|
||||||
danveitch76,
|
danveitch76,
|
||||||
@@ -1744,6 +1750,7 @@ ljsquare,
|
|||||||
llabourdeth,
|
llabourdeth,
|
||||||
llewy,
|
llewy,
|
||||||
llfjahn,
|
llfjahn,
|
||||||
|
llugo,
|
||||||
lmalmoreno,Brazil
|
lmalmoreno,Brazil
|
||||||
lnrdmx,
|
lnrdmx,
|
||||||
loc4t3llix,
|
loc4t3llix,
|
||||||
@@ -2050,6 +2057,7 @@ pedrolicassali,
|
|||||||
pedromfa,
|
pedromfa,
|
||||||
pedrware,Portugal
|
pedrware,Portugal
|
||||||
peeetek,
|
peeetek,
|
||||||
|
peggleg,
|
||||||
pejannl,
|
pejannl,
|
||||||
pem884,United States
|
pem884,United States
|
||||||
pepelatc,
|
pepelatc,
|
||||||
@@ -2273,6 +2281,7 @@ skalingclouds,United States
|
|||||||
skamaleo,
|
skamaleo,
|
||||||
skavieller,
|
skavieller,
|
||||||
skipper00,
|
skipper00,
|
||||||
|
skoducks,United States
|
||||||
skylidefr,
|
skylidefr,
|
||||||
skynet-network,
|
skynet-network,
|
||||||
slimehands,
|
slimehands,
|
||||||
@@ -2532,6 +2541,7 @@ williamcorsel,Netherlands
|
|||||||
willigenburggihaux,
|
willigenburggihaux,
|
||||||
willnewcombe,United Kingdom
|
willnewcombe,United Kingdom
|
||||||
wimb0,
|
wimb0,
|
||||||
|
wingerasc,
|
||||||
witold-gren,Poland
|
witold-gren,Poland
|
||||||
wonderfulhuber,
|
wonderfulhuber,
|
||||||
wonkygecko,United States
|
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 |
17
.github/workflows/onpr_check-pr.yaml
vendored
@@ -35,6 +35,7 @@ jobs:
|
|||||||
git fetch origin "${{ github.event.before }}" || true
|
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)
|
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 "$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)
|
changed_config_files=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" | grep -E '^[^/]+/config\.(json|ya?ml)$' || true)
|
||||||
echo "$changed_config_files"
|
echo "$changed_config_files"
|
||||||
all_changed_files=$(echo -e "$changed_config_files\n$changed_changelog_files" | sort -u)
|
all_changed_files=$(echo -e "$changed_config_files\n$changed_changelog_files" | sort -u)
|
||||||
@@ -92,6 +93,21 @@ jobs:
|
|||||||
- name: ↩️ Checkout
|
- name: ↩️ Checkout
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Copy templates into addon build context
|
||||||
|
env:
|
||||||
|
ADDON: ${{ matrix.addon }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TEMPLATES_DIR=".templates"
|
||||||
|
ADDON_DIR="./$ADDON"
|
||||||
|
|
||||||
|
# Keep PR builds aligned with the production builder.
|
||||||
|
for script in ha_automodules.sh ha_autoapps.sh ha_entrypoint.sh bashio-standalone.sh ha_lsio.sh; do
|
||||||
|
if [ -f "$TEMPLATES_DIR/$script" ]; then
|
||||||
|
cp "$TEMPLATES_DIR/$script" "$ADDON_DIR/$script"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
- name: ℹ️ Gather addon info
|
- name: ℹ️ Gather addon info
|
||||||
id: information
|
id: information
|
||||||
uses: frenck/action-addon-information@v1.4
|
uses: frenck/action-addon-information@v1.4
|
||||||
@@ -208,6 +224,7 @@ jobs:
|
|||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: ${{ matrix.addon }}
|
context: ${{ matrix.addon }}
|
||||||
|
platforms: linux/arm64
|
||||||
push: false
|
push: false
|
||||||
load: true
|
load: true
|
||||||
file: ${{ matrix.addon }}/Dockerfile
|
file: ${{ matrix.addon }}/Dockerfile
|
||||||
|
|||||||
3
.github/workflows/onpush_builder.yaml
vendored
@@ -300,7 +300,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build ${{ matrix.addon }} add-on
|
- name: Build ${{ matrix.addon }} add-on
|
||||||
if: steps.info.outputs.build_arch == 'true' && steps.info.outputs.has_dockerfile == 'true'
|
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:
|
with:
|
||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
cache-gha: "false"
|
cache-gha: "false"
|
||||||
@@ -433,4 +433,3 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
git push origin HEAD:master
|
git push origin HEAD:master
|
||||||
|
|
||||||
|
|||||||
51
.github/workflows/pr2841-diagnostic.yml
vendored
@@ -1,51 +0,0 @@
|
|||||||
---
|
|
||||||
name: Claude Add-on Build Diagnostic
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
diagnose:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
|
|
||||||
- name: Gather add-on information
|
|
||||||
id: information
|
|
||||||
uses: frenck/action-addon-information@v1.4
|
|
||||||
with:
|
|
||||||
path: ./claude_desktop/
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v4
|
|
||||||
|
|
||||||
- name: Build aarch64 and capture tail
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set +e
|
|
||||||
BUILD_FROM="$(jq -r '.build_from.aarch64 // empty' '${{ steps.information.outputs.build }}')"
|
|
||||||
docker buildx build \
|
|
||||||
--platform linux/arm64 \
|
|
||||||
--progress=plain \
|
|
||||||
--build-arg "BUILD_FROM=${BUILD_FROM}" \
|
|
||||||
--file claude_desktop/Dockerfile \
|
|
||||||
claude_desktop >build.log 2>&1
|
|
||||||
status=$?
|
|
||||||
tail -n 300 build.log >build-tail.log
|
|
||||||
printf 'build_status=%s\n' "$status" >build-status.txt
|
|
||||||
exit 0
|
|
||||||
|
|
||||||
- name: Upload diagnostic log
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: claude-aarch64-build-log
|
|
||||||
path: |
|
|
||||||
build-tail.log
|
|
||||||
build-status.txt
|
|
||||||
retention-days: 1
|
|
||||||
38
README.md
@@ -56,19 +56,19 @@ If you want to do add the repository manually, please follow the procedure highl
|
|||||||
|
|
||||||
### Number of addons
|
### Number of addons
|
||||||
|
|
||||||
- In the repository : 135
|
- In the repository : 136
|
||||||
- Installed : 508490
|
- Installed : 626324
|
||||||
|
|
||||||
### Top 3
|
### Top 3
|
||||||
|
|
||||||
1. Arpspoof (59486x)
|
1. Arpspoof (86665x)
|
||||||
2. Sponsorblockcast (55582x)
|
2. Sponsorblockcast (82801x)
|
||||||
3. Flaresolverr (44486x)
|
3. Jellyfin (71957x)
|
||||||
|
|
||||||
### Architectures used
|
### Architectures used
|
||||||
|
|
||||||
- amd64: 88%
|
- amd64: 90%
|
||||||
- aarch64: 12%
|
- aarch64: 10%
|
||||||
|
|
||||||
### Stars evolution
|
### 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
|
## Add-ons provided by this repository
|
||||||
|
|
||||||
%%ADDONS_LIST%%
|
%%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]
|
![amd64][amd64-badge]
|
||||||
![ingress][ingress-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
|
✓  [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]
|
![amd64][amd64-badge]
|
||||||
![ingress][ingress-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]
|
![aarch64][aarch64-badge]
|
||||||
![amd64][amd64-badge]
|
![amd64][amd64-badge]
|
||||||
![ingress][ingress-badge]
|
![ingress][ingress-badge]
|
||||||
|
![smb][smb-badge]
|
||||||
|
![localdisks][localdisks-badge]
|
||||||
|
|
||||||
✓  [Cleanuparr](cleanuparr/) : Automatically removes stuck and unwanted downloads from your *arr and download clients
|
✓  [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
|
✓ [Elasticsearch server](elasticsearch/) : Free and Open, Distributed, RESTful Search Engine
|
||||||
|
|
||||||
  
|
  
|
||||||
|

|
||||||
![aarch64][aarch64-badge]
|
![aarch64][aarch64-badge]
|
||||||
![amd64][amd64-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]
|
![smb][smb-badge]
|
||||||
![localdisks][localdisks-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]
|
![aarch64][aarch64-badge]
|
||||||
![amd64][amd64-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]
|
![smb][smb-badge]
|
||||||
![localdisks][localdisks-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)
|
## 1.76.51 (2026-06-17)
|
||||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||||
## 1.76.49 (2026-06-05)
|
## 1.76.49 (2026-06-05)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: Aurral
|
name: Aurral
|
||||||
version: "1.76.51"
|
version: "1.76.52"
|
||||||
slug: aurral
|
slug: aurral
|
||||||
description: >-
|
description: >-
|
||||||
Self-hosted music discovery, request management, flows, and playlist
|
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",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "aurral",
|
"slug": "aurral",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"upstream_repo": "lklynet/aurral",
|
"upstream_repo": "lklynet/aurral",
|
||||||
"upstream_version": "1.76.51",
|
"upstream_version": "1.76.52",
|
||||||
"github_beta": false
|
"github_beta": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
|
||||||
|
## 1.82.1 (2026-07-16)
|
||||||
|
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
||||||
|
|
||||||
## 1.81.0 (2026-07-04)
|
## 1.81.0 (2026-07-04)
|
||||||
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
||||||
## 1.80.0 (2026-06-05)
|
## 1.80.0 (2026-06-05)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
ARG BUILD_FROM
|
ARG BUILD_FROM
|
||||||
ARG BUILD_VERSION
|
ARG BUILD_VERSION
|
||||||
ARG BUILD_UPSTREAM="1.81.0"
|
ARG BUILD_UPSTREAM="1.82.1"
|
||||||
FROM ${BUILD_FROM}
|
FROM ${BUILD_FROM}
|
||||||
|
|
||||||
##################
|
##################
|
||||||
|
|||||||
@@ -108,4 +108,4 @@ schema:
|
|||||||
slug: autobrr
|
slug: autobrr
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/autobrr
|
url: https://github.com/alexbelgium/hassio-addons/tree/master/autobrr
|
||||||
version: "1.81.0"
|
version: "1.82.1"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"last_update": "2026-07-04",
|
"last_update": "2026-07-16",
|
||||||
"repository": "alexbelgium/hassio-addons",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "autborr",
|
"slug": "autborr",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"upstream_repo": "autobrr/autobrr",
|
"upstream_repo": "autobrr/autobrr",
|
||||||
"upstream_version": "1.81.0"
|
"upstream_version": "1.82.1"
|
||||||
}
|
}
|
||||||
|
|||||||
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,11 @@
|
|||||||
|
## source-20260717 (17-07-2026)
|
||||||
|
- Minor bugs fixed
|
||||||
|
## source-20260716.02 (16-07-2026)
|
||||||
|
- Minor bugs fixed
|
||||||
|
## source-20260716 (16-07-2026)
|
||||||
|
- Minor bugs fixed
|
||||||
|
## source-20260714 (14-07-2026)
|
||||||
|
- Minor bugs fixed
|
||||||
## source-20260709 (09-07-2026)
|
## source-20260709 (09-07-2026)
|
||||||
- Minor bugs fixed
|
- Minor bugs fixed
|
||||||
## source-20260708-4 (08-07-2026)
|
## source-20260708-4 (08-07-2026)
|
||||||
|
|||||||
@@ -127,5 +127,5 @@ slug: birdnet-go-dev
|
|||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
url: https://github.com/alexbelgium/hassio-addons
|
||||||
usb: true
|
usb: true
|
||||||
version: "source-20260709"
|
version: "source-20260717"
|
||||||
video: true
|
video: true
|
||||||
|
|||||||
@@ -77,10 +77,27 @@ for entry in "${prs[@]}"; do
|
|||||||
# Fetch the PR head commit by number; works unauthenticated for public repos.
|
# Fetch the PR head commit by number; works unauthenticated for public repos.
|
||||||
git fetch --no-tags origin "refs/pull/${number}/head"
|
git fetch --no-tags origin "refs/pull/${number}/head"
|
||||||
if ! git merge --no-edit --no-ff -m "Merge PR #${number}: ${title}" "${sha}"; then
|
if ! git merge --no-edit --no-ff -m "Merge PR #${number}: ${title}" "${sha}"; then
|
||||||
echo "!!! Merge conflict while merging PR #${number} (${title})." >&2
|
mapfile -t conflicted_files < <(git diff --name-only --diff-filter=U)
|
||||||
echo "!!! Resolve the conflict in the fork or pause this PR, then rebuild." >&2
|
|
||||||
git merge --abort || true
|
# package-lock.json is generated content and stacked PRs can carry an
|
||||||
exit 1
|
# 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
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1,3 +1,9 @@
|
|||||||
|
|
||||||
|
## 20260716 (2026-07-16)
|
||||||
|
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
|
||||||
|
|
||||||
|
## 20260712 (2026-07-13)
|
||||||
|
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
|
||||||
## nightly-20260615-4 (09-07-2026)
|
## nightly-20260615-4 (09-07-2026)
|
||||||
- Minor bugs fixed
|
- Minor bugs fixed
|
||||||
## nightly-20260615-3 (05-07-2026)
|
## nightly-20260615-3 (05-07-2026)
|
||||||
|
|||||||
@@ -128,4 +128,4 @@ slug: birdnet-go
|
|||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-go
|
url: https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-go
|
||||||
usb: true
|
usb: true
|
||||||
version: "nightly-20260615-4"
|
version: "20260716"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -2,10 +2,10 @@
|
|||||||
"github_beta": true,
|
"github_beta": true,
|
||||||
"github_exclude": "-4",
|
"github_exclude": "-4",
|
||||||
"github_fulltag": true,
|
"github_fulltag": true,
|
||||||
"last_update": "2026-06-17",
|
"last_update": "2026-07-16",
|
||||||
"repository": "alexbelgium/hassio-addons",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "birdnet-go",
|
"slug": "birdnet-go",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"upstream_repo": "tphakala/birdnet-go",
|
"upstream_repo": "tphakala/birdnet-go",
|
||||||
"upstream_version": "nightly-20260615"
|
"upstream_version": "20260716"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
## 2026.07.10 (10-07-2026)
|
||||||
- Minor bugs fixed
|
- Minor bugs fixed
|
||||||
## 2026.06.01 (19-06-2026)
|
## 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 && \
|
curl -f -L -s -S "https://raw.githubusercontent.com/alexbelgium/BirdNET-Pi/main/newinstaller.sh" -o /newinstaller.sh && \
|
||||||
chmod 777 /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
|
# Modify installer
|
||||||
##################
|
##################
|
||||||
# Use my repository
|
# Use my repository
|
||||||
@@ -110,6 +99,16 @@ RUN \
|
|||||||
# Execute installer
|
# Execute installer
|
||||||
/./newinstaller.sh && \
|
/./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
|
# Install dateparser and resampy, upgrade numpy
|
||||||
$PYTHON_VIRTUAL_ENV /usr/bin/pip3 install dateparser resampy && \
|
$PYTHON_VIRTUAL_ENV /usr/bin/pip3 install dateparser resampy && \
|
||||||
\
|
\
|
||||||
|
|||||||
@@ -116,5 +116,5 @@ tmpfs: true
|
|||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-pi
|
url: https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-pi
|
||||||
usb: true
|
usb: true
|
||||||
version: 2026.07.10
|
version: 2026.07.10-1
|
||||||
video: true
|
video: true
|
||||||
|
|||||||
@@ -4,6 +4,14 @@
|
|||||||
# Correct /config permissions after startup
|
# Correct /config permissions after startup
|
||||||
chown pi:pi /config
|
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.
|
# Set timezone without requiring Home Assistant Supervisor or D-Bus.
|
||||||
TZ_VALUE="${TZ:-}"
|
TZ_VALUE="${TZ:-}"
|
||||||
if [[ -S /var/run/dbus/system_bus_socket ]] && command -v timedatectl > /dev/null 2>&1; then
|
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)
|
## 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.
|
- 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?
|
ssl: bool?
|
||||||
slug: birdnet-pipy
|
slug: birdnet-pipy
|
||||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/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",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "birdnet-pipy",
|
"slug": "birdnet-pipy",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"upstream_repo": "Suncuss/BirdNET-PiPy",
|
"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,10 @@
|
|||||||
|
|
||||||
|
## 1.92.140-ls113 (2026-07-16)
|
||||||
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
|
## 1.92.139-ls112 (2026-07-11)
|
||||||
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
## 1.92.134-ls109 (2026-07-04)
|
## 1.92.134-ls109 (2026-07-04)
|
||||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
|
|||||||
@@ -69,5 +69,5 @@ slug: brave
|
|||||||
tmpfs: true
|
tmpfs: true
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
url: https://github.com/alexbelgium/hassio-addons
|
||||||
version: "1.92.134-ls109"
|
version: "1.92.140-ls113"
|
||||||
video: true
|
video: true
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"github_fulltag": "true",
|
"github_fulltag": "true",
|
||||||
"last_update": "2026-07-04",
|
"last_update": "2026-07-16",
|
||||||
"repository": "alexbelgium/hassio-addons",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "brave",
|
"slug": "brave",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"upstream_repo": "linuxserver/docker-brave",
|
"upstream_repo": "linuxserver/docker-brave",
|
||||||
"upstream_version": "1.92.134-ls109"
|
"upstream_version": "1.92.140-ls113"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
|
|
||||||
|
## version-ae9a7fca (2026-07-16)
|
||||||
|
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||||
|
|
||||||
|
## version-30a7c401 (2026-07-11)
|
||||||
|
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||||
|
|
||||||
## version-7148c2a3 (2026-07-04)
|
## version-7148c2a3 (2026-07-04)
|
||||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||||
|
|
||||||
|
|||||||
@@ -71,5 +71,5 @@ slug: chromium
|
|||||||
tmpfs: true
|
tmpfs: true
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
url: https://github.com/alexbelgium/hassio-addons
|
||||||
version: "version-7148c2a3"
|
version: "version-ae9a7fca"
|
||||||
video: true
|
video: true
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"github_fulltag": "true",
|
"github_fulltag": "true",
|
||||||
"last_update": "2026-07-04",
|
"last_update": "2026-07-16",
|
||||||
"repository": "alexbelgium/hassio-addons",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "chromium",
|
"slug": "chromium",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"upstream_repo": "linuxserver/docker-chromium",
|
"upstream_repo": "linuxserver/docker-chromium",
|
||||||
"upstream_version": "version-7148c2a3"
|
"upstream_version": "version-ae9a7fca"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
|
||||||
|
## 2.55.0 (2026-07-16)
|
||||||
|
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||||
|
|
||||||
## 2.54.2 (2026-07-04)
|
## 2.54.2 (2026-07-04)
|
||||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||||
|
|
||||||
|
|||||||
@@ -86,5 +86,5 @@ schema:
|
|||||||
slug: browserless_chrome
|
slug: browserless_chrome
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/browserless_chrome
|
url: https://github.com/alexbelgium/hassio-addons/tree/master/browserless_chrome
|
||||||
version: "2.54.2"
|
version: "2.55.0"
|
||||||
webui: "[PROTO:ssl]://[HOST]:[PORT:3000]/docs"
|
webui: "[PROTO:ssl]://[HOST]:[PORT:3000]/docs"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"github_tagfilter": "v",
|
"github_tagfilter": "v",
|
||||||
"last_update": "2026-07-04",
|
"last_update": "2026-07-16",
|
||||||
"repository": "alexbelgium/hassio-addons",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "browserless_chrome",
|
"slug": "browserless_chrome",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"upstream_repo": "browserless/chrome",
|
"upstream_repo": "browserless/chrome",
|
||||||
"upstream_version": "2.54.2"
|
"upstream_version": "2.55.0"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1,4 +1,7 @@
|
|||||||
|
|
||||||
|
## 0.55.8 (2026-07-16)
|
||||||
|
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
|
||||||
|
|
||||||
## 0.55.7 (2026-05-30)
|
## 0.55.7 (2026-05-30)
|
||||||
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
|
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
|
||||||
|
|
||||||
|
|||||||
@@ -34,4 +34,4 @@ schema:
|
|||||||
slug: changedetection.io
|
slug: changedetection.io
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/changedetection.io
|
url: https://github.com/alexbelgium/hassio-addons/tree/master/changedetection.io
|
||||||
version: "0.55.7"
|
version: "0.55.8"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"github_fulltag": "false",
|
"github_fulltag": "false",
|
||||||
"last_update": "2026-05-30",
|
"last_update": "2026-07-16",
|
||||||
"repository": "alexbelgium/hassio-addons",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "changedetection.io",
|
"slug": "changedetection.io",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"upstream_repo": "linuxserver/docker-changedetection.io",
|
"upstream_repo": "linuxserver/docker-changedetection.io",
|
||||||
"upstream_version": "0.55.7"
|
"upstream_version": "0.55.8"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,107 @@
|
|||||||
|
## 1.32 (17-07-2026)
|
||||||
|
|
||||||
|
- Bump `tokensave` from 7.2.0 to 7.4.0 (`rtk` was already pinned to its current latest GitHub release, `v0.43.0`; `headroom-ai` is intentionally installed unpinned from PyPI, so it already tracks latest at every build and had nothing to bump). Reviewed the intervening 7.3.0/7.4.0 release notes against every tokensave surface this add-on drives (`install --agent claude --git-hook yes`, `uninstall --agent claude`, `sync`, `init`, `doctor --agent claude`, `gain --all --range 30d`, and the `mcp__tokensave__*` tool set granted in `settings.json`): no flag, output shape, or MCP tool name used here changed. Directly relevant fixes carried along: `tokensave sync` auto-migrates a v12 database (missing the trait-dispatch caller cache) to v13 as a normal part of syncing, which the add-on's corruption-quarantine logic won't mistake for corruption since the schema migration doesn't produce a "malformed"/"not a database" error; and `install`/`uninstall`'s JSON writer now resolves a symlinked `~/.claude/settings.json` before its atomic rename instead of replacing the symlink with a plain file, so a dotfiles-managed settings file survives untouched.
|
||||||
|
- Fix startup TokenSave repository preparation silently doing nothing: both `81-tokensave_repositories.sh` and the indexing loop in `82-claude_tools.sh` read `tokensave_project_paths` through `done < <(bashio::config ...)`, but `bashio::config` begins with a `read -d ''` heredoc that always returns non-zero, and a process substitution inherits the `errexit` enabled by the bashio wrapper itself — so the subshell died before printing and every boot iterated over an empty list (no `safe.directory` persistence, no startup `sync`/`init`; command substitutions were unaffected because subshells drop `errexit` when `inherit_errexit` is off, which is why every other option lookup worked). Repositories only got indexed when tokensave's own git hooks or a manual `tokensave init` happened to run. The list is now captured with a command substitution first and the loop reads from the captured variable (here-string); same fix applied to `claude-tools-doctor.sh`. The indexing loop also moved ahead of the MCP-registration merge, and the merge now re-tightens the 0600 mode on the token-bearing configs even on no-change boots — a first-time `tokensave init` rewrites `~/.claude.json` itself at default permissions, which previously could leave the stored Home Assistant token world-readable until the next registration change.
|
||||||
|
- Simplification pass over the startup logic: every remaining line now serves a live purpose, with no change to what gets configured — Headroom (proxy routing, MCP registration, CLAUDE.md guidance, PostToolUse auto-compression, dashboard exposure), RTK (global files + PreToolUse hook), and TokenSave (full agent integration + per-repo indexing) are still applied automatically to every new session type (terminal, Desktop cowork/dispatch, cron).
|
||||||
|
- `82-claude_tools.sh`: the three hand-rolled `~/.claude/settings.json` hook mutators (rtk add, rtk remove, headroom PostToolUse) are replaced by one shared `manage_settings_hook` helper using the proven strip-then-re-append pass (same dedup/matcher-migration semantics; additionally no longer creates an empty `settings.json` when asked to remove a hook from a machine that never had one). The two copy-pasted CLAUDE.md guidance managers (headroom, ha-api-helper) collapse into one `manage_claude_md_block` helper producing byte-identical blocks, so existing installs are recognized without a rewrite.
|
||||||
|
- `81-tokensave_repositories.sh` is merged into the TokenSave loop of `82-claude_tools.sh`: the same path list was parsed twice with identical trimming/validation only so `safe.directory` could be persisted before repository detection ran as root. Detection now runs directly as the runtime user with a one-shot `safe.directory` override (the persisted entry is still written for tokensave's git hooks and Claude sessions), removing the duplicate loop and the root-reads-abc-gitconfig coupling. The battle-tested defensive sync/init block (flock, retries, corruption quarantine, init sentinel) is unchanged.
|
||||||
|
- The `/tmp/claude-desktop-command` indirection is gone: `82-claude_tools.sh` wrote the default launch command to a file that only `defaults/autostart` read, with the identical default hardcoded as its fallback — nothing else ever wrote it. `autostart` now launches Claude Desktop directly (keyring bootstrap unchanged).
|
||||||
|
- `82-claude_tools.sh` no longer ends with its own recursive chown of `~/.claude`, `~/.claude.json`, and `~/.config/Claude`: `84-claude_runtime_ownership.sh` already reconciles exactly those paths after all Claude configuration scripts have run.
|
||||||
|
- `83-claude_permissions.sh` drops the hidden `.addon-permission-mode.json` state file in favor of the same managed-value semantics used for `ANTHROPIC_BASE_URL`: `auto`/`bypass` set `permissions.defaultMode`, `strict` removes it only while it still holds an add-on-managed value (`auto`/`bypassPermissions`), and a hand-set custom value is never deleted. The old restore-from-state behavior could resurrect a stale value recorded on the first managed boot; the stale state file is cleaned up on upgrade.
|
||||||
|
- `80-configuration.sh` sheds branches that were unreachable in this image: `apk`/`pacman` installers (the base is Debian), the `pip3` fallback (`uv` is always baked in), and the no-op timezone error path (invalid `TZ` values are now actually detected against `/usr/share/zoneinfo` before the symlink is written).
|
||||||
|
- Remove the dead `auto_update` option from `config.yaml` and the README: its schema entry was removed back in 1.22 and `81-claude_update.sh` has updated Claude Desktop unconditionally (best-effort, offline-safe) ever since; the README now states that behavior instead of documenting a switch that did nothing.
|
||||||
|
|
||||||
|
## 1.31 (16-07-2026)
|
||||||
|
|
||||||
|
- Pin the LinuxServer selkies base image to a fixed version (`…-debianbookworm-45960cc3-ls113`) instead of the rolling `…-debianbookworm` tag. The rolling tag is rebuilt continuously (and itself installs selkies "latest" at base-build time), so the desktop/stream runtime could change under the add-on with no change to its own files — builds are now reproducible and the base only moves when this value is bumped deliberately. The pinned tags resolve to exactly the image the rolling tag currently points at (amd64 `sha256:6a4d5154…`, aarch64 `sha256:90914dfd…`).
|
||||||
|
- Fix Claude Desktop never appearing — the Selkies web client stayed on "waiting for stream" forever with `libEGL warning: failed to open /dev/dri/card0: Permission denied` in the log. The LinuxServer base image grants the desktop user (`abc`) access to the `/dev/dri` render nodes in its `init-video` s6 oneshot, but that oneshot is not a dependency of `svc-xorg`/`svc-selkies`/`svc-de`, so on Home Assistant those long-running services regularly start (via `s6-setuidgid abc`) *before* `abc` has been added to the render group. Xorg/Selkies/pixelflux then open the render device without permission, the video pipeline produces no frames, and the stream never starts. Prepare the exposed DRI nodes in a new `21-gpu_permissions.sh` cont-init script instead: `cont-init.d` runs to completion before any s6-rc service starts, so `abc` is added to each node's owning group (and the node is made world read/write as a timing-independent fallback) in time for the graphical services to use the GPU. Best-effort and a no-op on hosts that expose no GPU.
|
||||||
|
|
||||||
|
## 1.30 (16-07-2026)
|
||||||
|
|
||||||
|
- Compress large tool outputs automatically in every Claude Code session with a managed `PostToolUse` hook (new `headroom_auto_compress` option, enabled by default). Desktop-spawned sessions (cowork/dispatch) pin `ANTHROPIC_BASE_URL` to the production endpoint (headroom #869), so the transparent proxy never sees their traffic and compression there depended entirely on the model remembering to call the `headroom` MCP tools per the CLAUDE.md guidance — in practice most large outputs went uncompressed. The new `/usr/local/bin/headroom-posttooluse-compress.py` hook fires on `Bash`/`Grep`/`Glob`/`WebFetch` results over ~4000 characters, compresses them with Headroom's rule-based pipeline (SmartCrusher and friends; the Kompress ML path is disabled because its background model load can never complete inside a short-lived hook process), and swaps the result in via `hookSpecificOutput.updatedToolOutput` with a retrieval marker appended. Originals are stored in the shared CCR SQLite store (`~/.headroom/ccr_store.db` — the same one the headroom MCP server reads), so `mcp__headroom__headroom_retrieve` always recovers the full output; savings are recorded to the durable ledger (client `posttooluse-hook`) and show up in the existing gains report. The hook fails open (any error leaves the tool output untouched), never touches `stderr` fields so error text reaches the model verbatim, skips anything below a 50-token savings floor, and is registered idempotently in `~/.claude/settings.json` only after a `--self-test` confirms the interpreter can import headroom; disabling the option (or Headroom) removes the managed entry without touching user-defined hooks. Measured on a representative Home Assistant `states` dump: 10781 -> 2964 tokens (73% saved) at ~1.7 s hook overhead, with sub-100 ms pass-through for small outputs.
|
||||||
|
|
||||||
|
## 1.29 (16-07-2026)
|
||||||
|
|
||||||
|
- Point the Headroom MCP server at the persistent Kompress model cache. 1.27 set `HF_HOME` on the `svc-headroom` proxy longrun only, but the MCP server is a separate process spawned by Claude Desktop / Claude Code from the registered `mcpServers` entry, so it never inherited that export and kept resolving the HuggingFace cache to `~/.cache` — symlinked to tmpfs here and wiped on every restart. Its Kompress ML path therefore never found the model, re-downloaded ~270 MB into tmpfs on each boot, and lost it again on the next one; `headroom_compress` fell back to `router:noop` (unchanged output) on prose and other unstructured content. The managed `headroom` entry in both `claude_desktop_config.json` and `~/.claude.json` now carries `env.HF_HOME` pointing at the same `~/.headroom/hf` cache the proxy warms. Rule-based compression (SmartCrusher, structured tool output) was unaffected and worked throughout.
|
||||||
|
- Fix `~/.gitconfig` being written as `root` and left unreadable by the `abc` runtime user, which broke git for the user that actually runs it: every commit failed with `Author identity unknown` and the `gh` credential helper was invisible to authenticated pushes. `git config --global` ran as root during init and rewrites the file on every start, so `20-folders.sh`'s earlier recursive chown never stuck to it (`.config/gh` survived abc-owned only because the "already authenticated" branch skips rewriting it). The git/gh setup now runs as `abc` via `s6-setuidgid`, matching `81-tokensave_repositories.sh`, and reclaims any root-owned copies left by an earlier version before writing.
|
||||||
|
- Fix `~/.bashrc` accumulating stale `HOME`/`FM_HOME` exports when `data_location` changes. The idempotency guard only tested for the *current* `$LOCATION`, so changing the option and later changing it back appended a second block while leaving the first, and the last one written won for every interactive shell — leaving `$HOME` pointing at a directory the add-on no longer manages. Any tool that resolves config through `$HOME` then read the wrong path (`headroom doctor` reported `claude: not routed (no ~/.claude/settings.json)` against a correctly routed install, and bare `headroom` invocations created a stray `.headroom` tree under the old location). The block is now marker-delimited and rewritten from scratch on every boot, so it is idempotent across any number of `data_location` changes.
|
||||||
|
|
||||||
|
## 1.27 (15-07-2026)
|
||||||
|
|
||||||
|
- Route Claude Desktop cowork/local-agent-mode sessions through the Headroom proxy. Desktop spawns its bundled Claude Code binary at an absolute path (bypassing the add-on's PATH wrapper) with `ANTHROPIC_BASE_URL` pinned to the production endpoint, so those sessions never produced proxy savings. The add-on now manages `env.ANTHROPIC_BASE_URL` in `~/.claude/settings.json` — settings `env` entries replace inherited environment values at CLI startup — gated on `headroom_wrap_claude_code` and never overwriting a user-customized endpoint.
|
||||||
|
- Fix Headroom's Kompress compression engine never activating, which made even proxied traffic record zero token savings (e.g. 175 requests, 0 saved). The proxy's startup preload is deliberately cache-only, but the HuggingFace model cache defaulted to `~/.cache` — tmpfs in this add-on, wiped every restart — so the ONNX model (plus the separately fetched `answerdotai/ModernBERT-base` tokenizer) was never cached and the engine idled in "deferred" mode forever, misleadingly logged as `Kompress: not installed`. `svc-headroom` now points `HF_HOME` at persistent storage (`~/.headroom/hf`, ~270 MB); the proxy's own request path already downloads a missing model in the background on first use and passes requests through uncompressed until it lands, so no blocking startup pre-warm is needed — the port binds immediately either way, and Kompress activates within the first couple of requests on the first boot, then loads instantly on every boot after. The already-installed `proxy` extra's ONNX runtime is sufficient — the multi-gigabyte PyTorch `ml` extra is deliberately not installed.
|
||||||
|
|
||||||
|
## 1.26 (15-07-2026)
|
||||||
|
|
||||||
|
- Fix startup permission failures that prevented Claude Desktop from starting: storage was chowned to a hardcoded `1000:1000`, but the shared `abc` desktop user was never mapped to that UID. During init `abc` was still the image default (`911`), so TokenSave (`.claude.json.new`), RTK (`RTK.md`), nginx, PulseAudio, the Mesa shader cache, and Claude Desktop itself all hit `Permission denied`; the base image's `init-adduser` then remapped `abc` to root mid-startup (PUID/PGID were read from add-on options where they did not exist, falling back to `0`), which also made Claude Code reject `permission_mode: bypass`.
|
||||||
|
- Add `PUID`/`PGID` add-on options (default `1000:1000`) and remap `abc` to that identity at the very start of folder setup, before any ownership is applied and before any service resolves the user. The base image's `init-adduser` is pinned to the same effective identity so it can no longer remap `abc` mid-startup.
|
||||||
|
- In `permission_mode: bypass`, a configured `PUID: 0` automatically falls back to UID `1000` (Claude Code refuses bypass permissions as root), retaining the configured group.
|
||||||
|
- Fix `bashio::config.array: command not found` in the TokenSave repository setup, tools configuration, and `claude-tools-doctor.sh`: the function only exists in the repo's standalone bashio, not in the real bashio shipped in the image. Use `bashio::config`, which prints list entries one per line.
|
||||||
|
- Return managed Claude configuration files to the effective `abc` identity instead of the raw configured `PUID`/`PGID` (which previously fell back to `0` and left the files root-owned).
|
||||||
|
- Pre-create `/tmp/.X11-unix` with the standard sticky mode so Xorg, which runs as the non-root `abc` user on a tmpfs `/tmp`, no longer fails to create its socket directory (`_XSERVTransmkdir: euid != 0`).
|
||||||
|
|
||||||
|
## 1.25 (15-07-2026)
|
||||||
|
- Minor bugs fixed
|
||||||
|
## 1.24 (15-07-2026)
|
||||||
|
|
||||||
|
- Fix the `/usr/local/bin/claude` wrapper never routing terminal Claude Code sessions through the Headroom proxy: it hardcoded `HEADROOM_BIN="/usr/local/bin/headroom"` while the binary is installed at `/usr/bin/headroom`, so the executable check always failed and the wrapper fell back to launching Claude Code directly. Resolve the binary with `command -v headroom` instead.
|
||||||
|
- Harden startup TokenSave indexing so an interrupted `init`/`sync` or a hard add-on stop can no longer leave a corrupt semantic graph that fails every subsequent boot. Each configured repository is now prepared under a startup-scoped `flock` (serialised against overlapping restarts and mid-boot git sync hooks); an existing index is refreshed with a retried incremental `sync` (transient `SQLITE_BUSY` no longer looks like corruption); and only a genuinely unreadable index — or a half-written one flagged by an `init` sentinel — is quarantined to `.tokensave/corrupt-<timestamp>/` and rebuilt from scratch, so the graph self-heals instead of propagating corruption.
|
||||||
|
|
||||||
|
## 1.23 (15-07-2026)
|
||||||
|
|
||||||
|
- Add a `ha-cli` helper that lets Claude configure Home Assistant (automations, scripts, scenes, helpers, dashboards, area/label/floor/entity registries, and service calls) through the Home Assistant Core API instead of a filesystem mount. It authenticates automatically with the add-on's `SUPERVISOR_TOKEN` via the Supervisor Core-API proxy (no token setup), and deliberately cannot reach `configuration.yaml`/`secrets.yaml` or other add-ons' credentials. Toggle with the new `enable_ha_api_helper` option (default on), which also controls a managed guidance block appended to `~/.claude/CLAUDE.md`.
|
||||||
|
|
||||||
|
## 1.21 (15-07-2026)
|
||||||
|
|
||||||
|
- Fix Claude Code bypass permissions being rejected when the add-on uses its default root `PUID`.
|
||||||
|
- In `permission_mode: bypass`, remap the shared `abc` Desktop runtime to an unused non-root UID before storage ownership and Selkies startup, while retaining its configured primary group for mounted-path access.
|
||||||
|
- Make folder setup and final Claude configuration ownership follow the effective `abc` identity instead of the configured root UID.
|
||||||
|
- Drop root console invocations of the add-on's `/usr/local/bin/claude` wrapper to the non-root `abc` runtime before passing `--dangerously-skip-permissions`.
|
||||||
|
- Extend `claude-tools-doctor.sh` with configured/effective UID and GID checks for bypass mode.
|
||||||
|
|
||||||
|
## 1.20 (15-07-2026)
|
||||||
|
|
||||||
|
- Complete the TokenSave Claude Code integration at startup: install its MCP server, permissions, PreToolUse/UserPromptSubmit/Stop hooks, global guidance, and Git synchronization hooks instead of registering only `tokensave serve`.
|
||||||
|
- Add `tokensave_project_paths` for explicit per-repository initialization and incremental synchronization; no repositories are scanned or indexed unless listed.
|
||||||
|
- Route PATH-based Claude Code launches through the already-supervised Headroom proxy by default with a recursion-safe `/usr/local/bin/claude` wrapper; fall back to the official binary when the proxy is unavailable.
|
||||||
|
- Pass the local proxy URL explicitly to the Headroom MCP server, while retaining MCP-only integration for the Desktop Electron application.
|
||||||
|
- Keep the unauthenticated Headroom dashboard container-local by default; add `expose_headroom_dashboard` and leave port `8787/tcp` unmapped until explicitly enabled.
|
||||||
|
- Fix the hourly gains report so Headroom no longer suppresses RTK output, add TokenSave gains, and gate each tool on its actual add-on option.
|
||||||
|
- Add `claude-tools-doctor.sh` to inspect binaries, redacted MCP registrations, hooks, proxy health, routing, project indexes, and gains.
|
||||||
|
- Install local validation tools (`jq`, `shellcheck`, `yamllint`, current `hadolint`, and current `actionlint`) to reduce avoidable CI round-trips.
|
||||||
|
- Disable the unpinned third-party Caveman startup installer by default; it remains opt-in.
|
||||||
|
|
||||||
|
## 1.19 (14-07-2026)
|
||||||
|
- Minor bugs fixed
|
||||||
|
## 1.18 (14-07-2026)
|
||||||
|
|
||||||
|
- **Breaking:** remove the standalone Claude Code web terminal (ttyd/tmux service, port `7681`, and the `enable_terminal`, `terminal_username`, `terminal_password`, `terminal_workspace` options). The add-on is now built purely around Claude Desktop; Claude Code remains installed and powers Desktop cowork/dispatch sessions with the RTK hook, Caveman, and MCP servers intact. If the add-on refuses to start after the update, open its Configuration tab and re-save to drop the removed options.
|
||||||
|
- Remove the `claude-direct` and `claude-headroom` terminal wrapper scripts and the unused `ha_smart_context` and `dangerously_skip_permissions` options.
|
||||||
|
- Fix the Headroom dashboard being unreachable at `http://<host>:8787/dashboard`: the supervised proxy only listened on `127.0.0.1`; it now binds `0.0.0.0` so the mapped port works.
|
||||||
|
- Fix dispatch/remote sessions and sign-in persistence: install the missing `gnome-keyring` package. The existing keyring bootstrap silently no-oped without it, leaving Electron `safeStorage` unavailable ("cannot store allowlist cache"), so auth tokens and dispatch permission grants were lost on restart.
|
||||||
|
- Add the tokensave code-intelligence MCP server (pinned 7.2.0, built from source like RTK), registered for both Claude Desktop and Claude Code; disable with `install_tokensave: false`.
|
||||||
|
- Implement the Home Assistant MCP bridge for real: `enable_ha_mcp` plus new `ha_mcp_url`/`ha_mcp_token` options register Home Assistant's MCP Server integration in Claude through `mcp-proxy`, using the integration's stateless Streamable HTTP endpoint (`/api/mcp`).
|
||||||
|
- Write the Claude configuration files with `0600` permissions, since they hold the Home Assistant access token in clear text.
|
||||||
|
- Restrict the build-time `chmod +x` pass to the directories the add-on actually ships scripts in instead of traversing the whole image.
|
||||||
|
- Register add-on-managed MCP servers in Claude Code's `~/.claude.json` as well as Claude Desktop's config, without clobbering user-customized entries.
|
||||||
|
- Install `uv` and use it for the `additional_pip` option for much faster package installs.
|
||||||
|
|
||||||
|
## 1.16 (14-07-2026)
|
||||||
|
- Minor bugs fixed
|
||||||
|
## 1.15 (13-07-2026)
|
||||||
|
- Minor bugs fixed
|
||||||
|
|
||||||
|
## ubunturesolute-version-6dc44b0e (2026-07-13)
|
||||||
|
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
|
||||||
|
## 1.14 (10-07-2026)
|
||||||
|
|
||||||
|
- Build pinned RTK 0.43.0 source on Debian Bookworm for both architectures instead of installing the upstream arm64 release binary, which requires GLIBC 2.39 and cannot run in the add-on image.
|
||||||
|
- Execute `rtk --version` inside the final image during the Docker build so future ABI incompatibilities fail CI instead of surfacing at runtime.
|
||||||
|
- Validate the final Bookworm-built RTK binary in a native aarch64 image build.
|
||||||
|
- Correct the repository PR checks so changed changelog paths are exported and aarch64 images are built explicitly for `linux/arm64`.
|
||||||
|
|
||||||
|
## debiantrixie-version-c55d3809 (2026-07-11)
|
||||||
|
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
|
||||||
## 1.13 (10-07-2026)
|
## 1.13 (10-07-2026)
|
||||||
|
|
||||||
- Add the official Claude Code stable package, `tmux`, `ripgrep`, and a pinned upstream `ttyd` binary for both supported architectures.
|
- 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_FROM
|
||||||
ARG BUILD_VERSION
|
ARG BUILD_VERSION
|
||||||
|
ARG RTK_VERSION="v0.43.0"
|
||||||
|
ARG RTK_COMMIT="5a7880d404db8364d602f2ecdc41dd790f64013f"
|
||||||
|
ARG TOKENSAVE_VERSION="7.4.0"
|
||||||
|
|
||||||
|
# The upstream aarch64 release is cross-built on ubuntu-latest and requires
|
||||||
|
# GLIBC 2.39. Build the pinned source on Bookworm instead so it is compatible
|
||||||
|
# with the add-on runtime on both supported architectures.
|
||||||
|
FROM rust:1.91-bookworm AS rtk-builder
|
||||||
|
ARG RTK_VERSION
|
||||||
|
ARG RTK_COMMIT
|
||||||
|
RUN git clone --depth 1 --branch "${RTK_VERSION}" https://github.com/rtk-ai/rtk.git /src/rtk && \
|
||||||
|
test "$(git -C /src/rtk rev-parse HEAD)" = "${RTK_COMMIT}" && \
|
||||||
|
cd /src/rtk && \
|
||||||
|
cargo build --release --locked && \
|
||||||
|
install -D -m 0755 target/release/rtk /out/rtk && \
|
||||||
|
/out/rtk --version
|
||||||
|
|
||||||
|
# tokensave ships no Bookworm-compatible prebuilt binary either; build the pinned
|
||||||
|
# crates.io release from source so GLIBC matches the add-on runtime.
|
||||||
|
FROM rust:1.91-bookworm AS tokensave-builder
|
||||||
|
ARG TOKENSAVE_VERSION
|
||||||
|
RUN cargo install tokensave --version "${TOKENSAVE_VERSION}" --locked --root /out && \
|
||||||
|
/out/bin/tokensave --version
|
||||||
|
|
||||||
FROM ${BUILD_FROM}
|
FROM ${BUILD_FROM}
|
||||||
|
ARG BUILD_ARCH
|
||||||
|
|
||||||
##################
|
##################
|
||||||
# 2 Modify Image #
|
# 2 Modify Image #
|
||||||
@@ -45,17 +70,21 @@ RUN curl -fsSL --retry 3 --retry-delay 2 \
|
|||||||
# 3 Install apps #
|
# 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/ /
|
COPY rootfs/ /
|
||||||
RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \; && \
|
RUN find /etc/cont-init.d /etc/s6-overlay /defaults /usr/local/bin -type f \
|
||||||
chmod +x /usr/local/bin/claude-direct /usr/local/bin/claude-headroom /usr/local/bin/claude-terminal-shell
|
\( -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
|
# Uses /bin for compatibility purposes
|
||||||
# hadolint ignore=DL4005
|
# hadolint ignore=DL4005
|
||||||
RUN if [ ! -f /bin/sh ] && [ -f /usr/bin/sh ]; then ln -s /usr/bin/sh /bin/sh; fi && \
|
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
|
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 && \
|
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 /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 && \
|
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 \
|
git \
|
||||||
gh \
|
gh \
|
||||||
ripgrep \
|
ripgrep \
|
||||||
tmux && \
|
jq \
|
||||||
|
shellcheck \
|
||||||
|
yamllint && \
|
||||||
|
test -x /usr/bin/claude && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# ttyd is not packaged in Debian bookworm. Install the pinned upstream static binary.
|
# Install the current upstream hadolint and actionlint releases for both supported
|
||||||
ARG TTYD_VERSION="1.7.7"
|
ARG HADOLINT_VERSION=v2.14.0
|
||||||
|
ARG ACTIONLINT_VERSION=v1.7.12
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
case "$(dpkg --print-architecture)" in \
|
case "${BUILD_ARCH}" in \
|
||||||
amd64) ttyd_arch="x86_64" ;; \
|
amd64) hadolint_arch="x86_64"; actionlint_arch="amd64" ;; \
|
||||||
arm64) ttyd_arch="aarch64" ;; \
|
aarch64) hadolint_arch="arm64"; actionlint_arch="arm64" ;; \
|
||||||
*) echo "Unsupported architecture for ttyd: $(dpkg --print-architecture)" >&2; exit 1 ;; \
|
*) echo "Unsupported validation-tools architecture: ${BUILD_ARCH}" >&2; exit 1 ;; \
|
||||||
esac; \
|
esac; \
|
||||||
curl -fsSL --retry 3 --retry-delay 2 \
|
curl -fsSL --retry 3 --retry-delay 2 \
|
||||||
-o /usr/local/bin/ttyd \
|
-o /usr/local/bin/hadolint \
|
||||||
"https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.${ttyd_arch}"; \
|
"https://github.com/hadolint/hadolint/releases/download/${HADOLINT_VERSION}/hadolint-linux-${hadolint_arch}"; \
|
||||||
chmod 0755 /usr/local/bin/ttyd; \
|
chmod 0755 /usr/local/bin/hadolint; \
|
||||||
/usr/local/bin/ttyd --version
|
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 && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends nodejs && \
|
apt-get install -y --no-install-recommends nodejs && \
|
||||||
pip3 install --break-system-packages "headroom-ai[proxy,code,mcp]" && \
|
pip3 install --break-system-packages "headroom-ai[proxy,code,mcp]" mcp-proxy uv websockets && \
|
||||||
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh -o /tmp/rtk-install.sh && \
|
|
||||||
HOME=/root sh /tmp/rtk-install.sh && \
|
|
||||||
rm /tmp/rtk-install.sh && \
|
|
||||||
if [ -x /root/.local/bin/rtk ] && [ ! -x /usr/local/bin/rtk ]; then mv /root/.local/bin/rtk /usr/local/bin/rtk; fi && \
|
|
||||||
if [ -x /usr/local/bin/rtk ]; then chmod +x /usr/local/bin/rtk; else echo "rtk binary was not installed on PATH"; exit 1; fi && \
|
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/* /root/.cache
|
rm -rf /var/lib/apt/lists/* /root/.cache
|
||||||
|
|
||||||
@@ -145,7 +192,6 @@ CMD [ "/ha_entrypoint.sh" ]
|
|||||||
# 5 Labels #
|
# 5 Labels #
|
||||||
############
|
############
|
||||||
|
|
||||||
ARG BUILD_ARCH
|
|
||||||
ARG BUILD_DATE
|
ARG BUILD_DATE
|
||||||
ARG BUILD_DESCRIPTION
|
ARG BUILD_DESCRIPTION
|
||||||
ARG BUILD_NAME
|
ARG BUILD_NAME
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
![Supports amd64 Architecture][amd64-shield]
|
![Supports amd64 Architecture][amd64-shield]
|
||||||
![Project Maintenance][maintenance-shield]
|
![Project Maintenance][maintenance-shield]
|
||||||
|
|
||||||
Run Claude Desktop and an optional persistent Claude Code web terminal in one
|
Run Claude Desktop in a LinuxServer.io Selkies add-on, with Headroom context
|
||||||
LinuxServer.io Selkies add-on.
|
compression, RTK Bash-output acceleration, and TokenSave semantic code
|
||||||
|
intelligence wired in by default.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -20,151 +21,252 @@ currently does not include Computer Use or dictation.
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Claude Desktop and Claude Code run as separate clients inside the same add-on.
|
Everything is built around the Claude Desktop app. Claude Code is installed in
|
||||||
They share the configured persistent home directory, Git credentials,
|
the same image but is not exposed as a standalone service: Claude Desktop's
|
||||||
repositories, Claude Code configuration, Headroom storage, and RTK
|
cowork and dispatch sessions run it internally, and they pick up the shared
|
||||||
configuration, but they do not share or hand off a conversation.
|
Claude Code configuration (`~/.claude`), hooks, MCP servers, permissions, and
|
||||||
|
PATH tools.
|
||||||
|
|
||||||
- **Claude Desktop** uses Headroom through its MCP tools.
|
- **Claude Desktop** uses Headroom through its MCP tools.
|
||||||
- **Claude Code** uses Headroom's supported `headroom wrap claude` integration.
|
- **Claude Code sessions inside Desktop** get the same MCP servers, permission
|
||||||
- **RTK** filters Claude Code Bash output through its `PreToolUse` hook.
|
mode, and RTK/TokenSave hooks through the shared Claude Code configuration.
|
||||||
- **tmux** keeps the terminal session running when the browser disconnects.
|
- 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
|
## Features
|
||||||
|
|
||||||
- Claude Desktop in single-app Selkies mode.
|
- Claude Desktop in single-app Selkies mode with Home Assistant ingress.
|
||||||
- Home Assistant ingress support for Claude Desktop.
|
- Official Claude Code stable package powering Desktop cowork/dispatch
|
||||||
- Official Claude Code stable package installed in the same image.
|
sessions.
|
||||||
- Optional authenticated `ttyd` web terminal on port `7681`.
|
|
||||||
- Persistent `tmux` session shared by reconnecting terminal clients.
|
|
||||||
- Persistent `$HOME` at the configured `data_location` (default `/data/data`),
|
- Persistent `$HOME` at the configured `data_location` (default `/data/data`),
|
||||||
preserving Desktop and Claude Code state across restarts.
|
preserving Desktop and Claude Code state across restarts.
|
||||||
- Optional runtime Claude Desktop updates from Anthropic's apt repository.
|
- Persistent sign-in through a bundled, auto-unlocked gnome-keyring.
|
||||||
- Optional extra apt and pip package installation.
|
- Configurable Claude Code permissions: strict prompts, automatic safe-action
|
||||||
- Baked-in `git`, GitHub CLI (`gh`), `ripgrep`, and terminal tooling.
|
approval, or explicit full bypass for trusted installations.
|
||||||
|
- Automatic non-root runtime enforcement for bypass mode, including root-console
|
||||||
|
wrapper launches.
|
||||||
|
- Best-effort Claude Desktop update from Anthropic's apt repository at every
|
||||||
|
startup (skipped silently when offline).
|
||||||
|
- Optional extra apt and pip package installation (pip installs use `uv`).
|
||||||
|
- Baked-in `git`, GitHub CLI (`gh`), `ripgrep`, `jq`, `shellcheck`, `yamllint`,
|
||||||
|
`hadolint`, and `actionlint`.
|
||||||
- Custom script support through the repository standard `claude_desktop.sh`.
|
- Custom script support through the repository standard `claude_desktop.sh`.
|
||||||
- Optional bundled Claude Code optimization tools: Headroom, RTK, and Caveman.
|
- Bundled optimization tools: Headroom, RTK, and TokenSave; Caveman remains
|
||||||
- Headroom dashboard exposed on mapped port `8787` when enabled.
|
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.
|
- 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
|
## Options
|
||||||
|
|
||||||
| Option | Default | Description |
|
| 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`. |
|
| `TZ` | | Optional timezone, for example `Europe/Brussels`. |
|
||||||
| `KEYBOARD` | | Optional Selkies keyboard layout. |
|
| `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. |
|
| `DRINODE` | | Optional GPU device override for Selkies. |
|
||||||
| `DNS_server` | `8.8.8.8` | DNS server used by the standard DNS module. |
|
| `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. |
|
| `permission_mode` | `auto` | Claude Code permission policy: `strict`, `auto`, or `bypass`. |
|
||||||
| `enable_terminal` | `true` | Enable the supervised Claude Code web-terminal service. |
|
| `install_headroom` | `true` | Register Headroom MCP and run the supervised local proxy. |
|
||||||
| `terminal_username` | `claude` | Username used by ttyd Basic Authentication. |
|
| `headroom_wrap_claude_code` | `true` | Route PATH-based Claude Code launches through the already-running Headroom proxy. |
|
||||||
| `terminal_password` | | Dedicated terminal password. The service idles when this and `PASSWORD` are empty. |
|
| `headroom_auto_compress` | `true` | Auto-compress large tool outputs in every Claude Code session via a managed `PostToolUse` hook. |
|
||||||
| `terminal_workspace` | | Initial directory; defaults to `<data_location>/workspace`. |
|
| `expose_headroom_dashboard` | `false` | Bind Headroom to all interfaces. Port `8787/tcp` must also be mapped manually. |
|
||||||
| `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` Bash hook. |
|
||||||
| `install_rtk` | `true` | Configure RTK's Claude Code `PreToolUse` hook. |
|
| `install_tokensave` | `true` | Install TokenSave's complete global Claude integration. |
|
||||||
| `install_caveman` | `true` | Install the Caveman Claude Code plugin in the persistent Claude home. |
|
| `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. |
|
| `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_token` | | Optional GitHub token used to authenticate `gh` and Git operations. |
|
||||||
| `github_username` | | Optional global Git author name. |
|
| `github_username` | | Optional global Git author name. |
|
||||||
| `github_email` | | Optional global Git author email. |
|
| `github_email` | | Optional global Git author email. |
|
||||||
| `ha_smart_context` | `true` | Enable Home Assistant smart context support for Claude tooling. |
|
| `enable_ha_mcp` | `false` | Register Home Assistant's MCP server in Claude (requires `ha_mcp_token`). |
|
||||||
| `enable_ha_mcp` | `true` | Enable Home Assistant MCP support for Claude tooling. |
|
| `ha_mcp_url` | `http://homeassistant:8123/api/mcp` | Streamable HTTP endpoint of Home Assistant's MCP Server integration. |
|
||||||
| `dangerously_skip_permissions` | `false` | Reserved compatibility option; it is not applied by the terminal launcher. |
|
| `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_apps` | | Comma-separated Debian apt packages to install at startup. |
|
||||||
| `additional_pip` | | Comma-separated pip packages installed with `--break-system-packages`. |
|
| `additional_pip` | | Comma-separated pip packages installed at startup (via `uv`). |
|
||||||
| `data_location` | `/data/data` | Persistent home directory for both Claude clients and tooling. |
|
| `data_location` | `/data/data` | Persistent home directory for Claude and tooling. |
|
||||||
| `env_vars` | `[]` | Additional environment variables exported inside the container. |
|
| `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
|
## Headroom behavior
|
||||||
|
|
||||||
When `install_headroom` is enabled, the add-on registers `headroom mcp serve` in
|
When `install_headroom` is enabled, the add-on registers `headroom mcp serve`
|
||||||
Claude Desktop and starts a supervised local Headroom backend. Desktop can use
|
with the explicit local proxy URL in Claude Desktop and Claude Code, then starts
|
||||||
`headroom_compress`, `headroom_retrieve`, and `headroom_stats` through MCP.
|
a supervised Headroom backend on `127.0.0.1:8787`.
|
||||||
|
|
||||||
Claude Desktop overrides `ANTHROPIC_BASE_URL`, so it is deliberately launched
|
Claude Desktop overrides `ANTHROPIC_BASE_URL`, so Desktop chat deliberately uses
|
||||||
without proxy injection. The web terminal instead provides `claude-headroom`,
|
the MCP integration. The `/usr/local/bin/claude` wrapper routes PATH-based Claude
|
||||||
which reuses the supervised proxy through Headroom's `--no-proxy` mode. RTK
|
Code sessions through `headroom wrap claude --no-proxy`, reusing the supervised
|
||||||
setup remains owned by the add-on through Headroom's `--no-rtk` mode.
|
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
|
The dashboard is disabled externally by default. To expose it:
|
||||||
http://<home-assistant-host>:8787/dashboard
|
|
||||||
|
1. Set `expose_headroom_dashboard: true`.
|
||||||
|
2. Map `8787/tcp` in the add-on **Network** section.
|
||||||
|
3. Open `http://<home-assistant-host>:8787/dashboard`.
|
||||||
|
|
||||||
|
The dashboard is unauthenticated. Do not publish this port to the public
|
||||||
|
internet.
|
||||||
|
|
||||||
|
## Diagnostics
|
||||||
|
|
||||||
|
Run the following inside the add-on through a custom script or container console:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude-tools-doctor.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
when the `8787/tcp` port is mapped. Treat this endpoint as sensitive and do not
|
The report checks the tool binaries, configuration switches, configured and
|
||||||
expose it directly to the public internet.
|
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
|
## Custom scripts
|
||||||
|
|
||||||
@@ -178,10 +280,14 @@ the image.
|
|||||||
Persistent state is stored in the configured `data_location` (default
|
Persistent state is stored in the configured `data_location` (default
|
||||||
`/data/data`):
|
`/data/data`):
|
||||||
|
|
||||||
- Claude Desktop sign-in: `~/.config/Claude`
|
- Claude Desktop sign-in: `~/.config/Claude` (token encrypted via
|
||||||
- Claude Code settings, hooks, sessions, and plugins: `~/.claude`
|
gnome-keyring; keyring DB in `~/.local/share/keyrings`)
|
||||||
- Default terminal workspace: `~/workspace`
|
- Claude Code settings, hooks, sessions, plugins, and permission mode:
|
||||||
- Headroom and RTK user state: their standard paths below the shared home
|
`~/.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
|
Volatile cache data is redirected to `/tmp/cache` through `$XDG_CACHE_HOME` and
|
||||||
`$HOME/.cache`.
|
`$HOME/.cache`.
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ Two related sign-in problems when Claude Desktop runs inside the LinuxServer Sel
|
|||||||
streamed desktop.
|
streamed desktop.
|
||||||
|
|
||||||
**Status:**
|
**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.
|
- **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.
|
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)
|
### User-side workaround (no rebuild)
|
||||||
- Add-on Configuration → `additional_apps: chromium`, restart (installed by
|
- Add-on Configuration → `additional_apps: chromium`, restart (installed by
|
||||||
`rootfs/etc/cont-init.d/80-configuration.sh`).
|
`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
|
- Add the two `xdg-settings`/`xdg-mime` commands to the custom script
|
||||||
to the custom script `/addon_configs/db21ed7f_claude-desktop/claude-desktop.sh`.
|
`/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`.
|
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
|
- `--password-store=gnome-libsecret` forces Electron to use the libsecret backend instead
|
||||||
of falling back to plaintext.
|
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
|
(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.
|
re-unlocked automatically each boot by the same `autostart` line — the sign-in then sticks.
|
||||||
|
|
||||||
### User-side workaround (no rebuild)
|
### User-side workaround (no rebuild)
|
||||||
- Add-on Configuration → `additional_apps: gnome-keyring, libsecret-1-0, dbus-x11`, restart.
|
- Add-on Configuration → `additional_apps: gnome-keyring, libsecret-1-0, dbus-x11`, restart.
|
||||||
- Add the keyring-start lines above to the custom script
|
- 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).
|
with `--password-store=gnome-libsecret` (e.g. edit the in-session openbox autostart).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"build_from": {
|
"build_from": {
|
||||||
"aarch64": "ghcr.io/linuxserver/baseimage-selkies:arm64v8-debianbookworm",
|
"aarch64": "ghcr.io/linuxserver/baseimage-selkies:arm64v8-debianbookworm-45960cc3-ls113",
|
||||||
"amd64": "ghcr.io/linuxserver/baseimage-selkies:amd64-debianbookworm"
|
"amd64": "ghcr.io/linuxserver/baseimage-selkies:amd64-debianbookworm-45960cc3-ls113"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ arch:
|
|||||||
- aarch64
|
- aarch64
|
||||||
- amd64
|
- amd64
|
||||||
audio: true
|
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:
|
devices:
|
||||||
- /dev/dri
|
- /dev/dri
|
||||||
- /dev/dri/card0
|
- /dev/dri/card0
|
||||||
@@ -13,8 +13,8 @@ environment:
|
|||||||
AUTO_GPU: "1"
|
AUTO_GPU: "1"
|
||||||
FM_HOME: /data/data
|
FM_HOME: /data/data
|
||||||
HOME: /data/data
|
HOME: /data/data
|
||||||
PGID: "0"
|
PGID: "1000"
|
||||||
PUID: "0"
|
PUID: "1000"
|
||||||
SELKIES_FRAMERATE: "30"
|
SELKIES_FRAMERATE: "30"
|
||||||
START_DOCKER: "false"
|
START_DOCKER: "false"
|
||||||
TITLE: Claude Desktop
|
TITLE: Claude Desktop
|
||||||
@@ -34,36 +34,37 @@ name: Claude Desktop
|
|||||||
options:
|
options:
|
||||||
env_vars: []
|
env_vars: []
|
||||||
DNS_server: 8.8.8.8
|
DNS_server: 8.8.8.8
|
||||||
|
PGID: 1000
|
||||||
|
PUID: 1000
|
||||||
data_location: /data/data
|
data_location: /data/data
|
||||||
PGID: 0
|
|
||||||
PUID: 0
|
|
||||||
additional_apps: ""
|
additional_apps: ""
|
||||||
additional_pip: ""
|
additional_pip: ""
|
||||||
auto_update: true
|
|
||||||
github_email: ""
|
github_email: ""
|
||||||
ha_smart_context: true
|
enable_ha_mcp: false
|
||||||
enable_ha_mcp: true
|
ha_mcp_url: http://homeassistant:8123/api/mcp
|
||||||
dangerously_skip_permissions: false
|
ha_mcp_token: ""
|
||||||
enable_terminal: true
|
enable_ha_api_helper: true
|
||||||
terminal_username: claude
|
|
||||||
terminal_password: ""
|
|
||||||
terminal_workspace: ""
|
|
||||||
github_token: ""
|
github_token: ""
|
||||||
github_username: ""
|
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_github_cli: true
|
||||||
install_headroom: true
|
install_headroom: true
|
||||||
install_rtk: true
|
install_rtk: true
|
||||||
|
install_tokensave: true
|
||||||
|
permission_mode: auto
|
||||||
|
tokensave_project_paths: []
|
||||||
panel_admin: false
|
panel_admin: false
|
||||||
panel_icon: mdi:robot-happy
|
panel_icon: mdi:robot-happy
|
||||||
ports:
|
ports:
|
||||||
3001/tcp: null
|
3001/tcp: null
|
||||||
7681/tcp: null
|
8787/tcp: null
|
||||||
8787/tcp: 8787
|
|
||||||
ports_description:
|
ports_description:
|
||||||
3001/tcp: Claude Desktop web interface
|
3001/tcp: Claude Desktop web interface
|
||||||
7681/tcp: HTTP Basic-auth Claude Code terminal (no TLS)
|
8787/tcp: Optional Headroom dashboard and proxy
|
||||||
8787/tcp: Headroom dashboard and proxy
|
|
||||||
privileged:
|
privileged:
|
||||||
- SYS_ADMIN
|
- SYS_ADMIN
|
||||||
- DAC_READ_SEARCH
|
- DAC_READ_SEARCH
|
||||||
@@ -81,24 +82,33 @@ schema:
|
|||||||
TZ: match([A-Z][a-z]*./[A-Z][a-z]*.)?
|
TZ: match([A-Z][a-z]*./[A-Z][a-z]*.)?
|
||||||
additional_apps: str?
|
additional_apps: str?
|
||||||
additional_pip: str?
|
additional_pip: str?
|
||||||
auto_update: bool?
|
cifsdomain: str?
|
||||||
|
cifspassword: str?
|
||||||
|
cifsusername: str?
|
||||||
|
localdisks: str?
|
||||||
|
networkdisks: str?
|
||||||
github_email: str?
|
github_email: str?
|
||||||
ha_smart_context: bool?
|
|
||||||
enable_ha_mcp: bool?
|
enable_ha_mcp: bool?
|
||||||
dangerously_skip_permissions: bool?
|
ha_mcp_url: str?
|
||||||
enable_terminal: bool?
|
ha_mcp_token: password?
|
||||||
terminal_username: match(^[A-Za-z0-9_.-]+$)?
|
enable_ha_api_helper: bool?
|
||||||
terminal_password: password?
|
|
||||||
terminal_workspace: str?
|
|
||||||
github_token: password?
|
github_token: password?
|
||||||
github_username: str?
|
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_caveman: bool
|
||||||
install_github_cli: bool
|
install_github_cli: bool
|
||||||
install_headroom: bool
|
install_headroom: bool
|
||||||
install_rtk: bool
|
install_rtk: bool
|
||||||
|
install_tokensave: bool
|
||||||
|
permission_mode: list(strict|auto|bypass)
|
||||||
|
tokensave_project_paths:
|
||||||
|
- str
|
||||||
slug: claude_desktop
|
slug: claude_desktop
|
||||||
tmpfs: true
|
tmpfs: true
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
url: https://github.com/alexbelgium/hassio-addons
|
||||||
version: "1.13"
|
version: "1.32"
|
||||||
video: true
|
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 |
@@ -12,24 +12,7 @@ export GNOME_KEYRING_CONTROL SSH_AUTH_SOCK
|
|||||||
dbus-update-activation-environment --all >/dev/null 2>&1 || true
|
dbus-update-activation-environment --all >/dev/null 2>&1 || true
|
||||||
|
|
||||||
# --password-store=gnome-libsecret forces Electron onto the libsecret backend instead of
|
# --password-store=gnome-libsecret forces Electron onto the libsecret backend instead of
|
||||||
# falling back to plaintext (and warning that the sign-in will not be saved).
|
# falling back to plaintext (and warning that the sign-in will not be saved). Headroom is
|
||||||
CLAUDE_DESKTOP_COMMAND_FILE="/tmp/claude-desktop-command"
|
# intentionally not injected into the Desktop process: Claude Desktop force-overrides
|
||||||
DEFAULT_CLAUDE_DESKTOP_COMMAND="claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret"
|
# ANTHROPIC_BASE_URL (headroom #869), so Desktop uses the registered Headroom MCP tools.
|
||||||
if [ -s "$CLAUDE_DESKTOP_COMMAND_FILE" ]; then
|
exec claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret
|
||||||
CLAUDE_DESKTOP_COMMAND="$(cat "$CLAUDE_DESKTOP_COMMAND_FILE")"
|
|
||||||
else
|
|
||||||
CLAUDE_DESKTOP_COMMAND="$DEFAULT_CLAUDE_DESKTOP_COMMAND"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Headroom is intentionally not injected into the Desktop process. Claude Desktop overrides
|
|
||||||
# ANTHROPIC_BASE_URL, so Desktop uses the registered Headroom MCP tools instead. The Claude Code
|
|
||||||
# terminal uses the supported `headroom wrap claude` integration through claude-headroom.
|
|
||||||
|
|
||||||
# Launch the configured command. If a custom/wrapped command fails to start, fall back to
|
|
||||||
# the plain Claude Desktop launch so the app always comes up for the user.
|
|
||||||
if ! sh -c "$CLAUDE_DESKTOP_COMMAND"; then
|
|
||||||
if [ "$CLAUDE_DESKTOP_COMMAND" != "$DEFAULT_CLAUDE_DESKTOP_COMMAND" ]; then
|
|
||||||
echo "autostart: '$CLAUDE_DESKTOP_COMMAND' failed; falling back to default Claude Desktop launch" >&2
|
|
||||||
exec sh -c "$DEFAULT_CLAUDE_DESKTOP_COMMAND"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -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
|
# 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.
|
# 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
|
0 * * * * /usr/local/bin/claude-gains-report.sh > /proc/1/fd/1 2>&1
|
||||||
|
|||||||
@@ -3,9 +3,35 @@
|
|||||||
# shellcheck disable=SC2046
|
# shellcheck disable=SC2046
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Define user
|
# Align the shared desktop user (abc) with the configured PUID/PGID before any storage is
|
||||||
PUID=$(bashio::config "PUID")
|
# chowned and before any service or s6-setuidgid call resolves abc. The base image's
|
||||||
PGID=$(bashio::config "PGID")
|
# 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
|
# Check data location
|
||||||
LOCATION="$(bashio::config '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" "$LOCATION" > "$S6_ENVDIR/FM_HOME"
|
||||||
printf "%s" "/tmp/cache" > "$S6_ENVDIR/XDG_CACHE_HOME"
|
printf "%s" "/tmp/cache" > "$S6_ENVDIR/XDG_CACHE_HOME"
|
||||||
printf "%s" "$XDG_RUNTIME_DIR" > "$S6_ENVDIR/XDG_RUNTIME_DIR"
|
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 HOME=\"$LOCATION\""
|
||||||
printf "%s\n" "export FM_HOME=\"$LOCATION\""
|
printf "%s\n" "export FM_HOME=\"$LOCATION\""
|
||||||
printf "%s\n" "export XDG_CACHE_HOME=\"/tmp/cache\""
|
printf "%s\n" "export XDG_CACHE_HOME=\"/tmp/cache\""
|
||||||
|
printf "%s\n" "$BASHRC_HOME_END"
|
||||||
} >> ~/.bashrc
|
} >> ~/.bashrc
|
||||||
|
|
||||||
bashio::log.info "Creating $LOCATION"
|
bashio::log.info "Creating $LOCATION"
|
||||||
@@ -70,6 +109,11 @@ mkdir -p "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR"
|
|||||||
chmod 755 /tmp/cache
|
chmod 755 /tmp/cache
|
||||||
chmod 700 "$XDG_RUNTIME_DIR"
|
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*"
|
# 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.
|
# calls (in init-selkies-config and svc-de) never fail on an empty glob.
|
||||||
touch /tmp/selkies_js.log
|
touch /tmp/selkies_js.log
|
||||||
@@ -81,7 +125,7 @@ fi
|
|||||||
ln -sfn /tmp/cache "$LOCATION/.cache"
|
ln -sfn /tmp/cache "$LOCATION/.cache"
|
||||||
|
|
||||||
bashio::log.info "Setting ownership to $PUID:$PGID"
|
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"
|
chmod -R 700 "$LOCATION"
|
||||||
|
|
||||||
# The base init-selkies-config script overrides XDG_RUNTIME_DIR to $HOME/.XDG, which lands
|
# The base init-selkies-config script overrides XDG_RUNTIME_DIR to $HOME/.XDG, which lands
|
||||||
|
|||||||
52
claude_desktop/rootfs/etc/cont-init.d/21-gpu_permissions.sh
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/with-contenv bashio
|
||||||
|
# shellcheck shell=bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Grant the shared desktop user (abc) access to the exposed GPU render nodes *before* the
|
||||||
|
# graphical services start.
|
||||||
|
#
|
||||||
|
# The LinuxServer base image already sets up /dev/dri group access, but it does so in its
|
||||||
|
# init-video s6 oneshot, which is NOT a dependency of svc-xorg/svc-selkies/svc-de. On Home
|
||||||
|
# Assistant those long-running services routinely start (as abc, via s6-setuidgid) before
|
||||||
|
# init-video has added abc to the render group, so Xorg/Selkies/pixelflux open the render
|
||||||
|
# device with the wrong credentials and fail:
|
||||||
|
#
|
||||||
|
# libEGL warning: failed to open /dev/dri/card0: Permission denied
|
||||||
|
#
|
||||||
|
# With no usable render node the video pipeline never produces frames, so the Selkies web
|
||||||
|
# client stays on "waiting for stream" indefinitely and Claude Desktop never appears.
|
||||||
|
#
|
||||||
|
# cont-init.d runs to completion before any s6-rc service is started, so preparing the DRI
|
||||||
|
# nodes here wins that race. Everything is best-effort: a host that exposes no GPU simply has
|
||||||
|
# no nodes to touch and this is a no-op.
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
dri_nodes=(/dev/dri/card* /dev/dri/render*)
|
||||||
|
if [ "${#dri_nodes[@]}" -eq 0 ]; then
|
||||||
|
bashio::log.info "No /dev/dri render nodes exposed; skipping GPU permission setup"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for node in "${dri_nodes[@]}"; do
|
||||||
|
[ -e "$node" ] || continue
|
||||||
|
|
||||||
|
# Mirror the base image's init-video logic (add abc to the node's owning group, creating
|
||||||
|
# the group when the GID is unnamed) but early enough that the s6-setuidgid at service
|
||||||
|
# start picks the membership up.
|
||||||
|
gid="$(stat -c '%g' "$node")"
|
||||||
|
gname="$(getent group "$gid" | awk -F: '{print $1}')"
|
||||||
|
if [ -z "$gname" ]; then
|
||||||
|
gname="dri${gid}"
|
||||||
|
groupadd -o -g "$gid" "$gname" 2> /dev/null || true
|
||||||
|
fi
|
||||||
|
if ! id -G abc 2> /dev/null | tr ' ' '\n' | grep -qx "$gid"; then
|
||||||
|
usermod -a -G "$gname" abc 2> /dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Guarantee access even where group propagation is unreliable inside the add-on sandbox:
|
||||||
|
# this is a single-user desktop container, so world read/write on the local render node
|
||||||
|
# is acceptable and removes any dependency on group-membership timing.
|
||||||
|
chmod o+rw "$node" 2> /dev/null || true
|
||||||
|
|
||||||
|
bashio::log.info "GPU: prepared ${node} (group ${gname}:${gid}) for the desktop user"
|
||||||
|
done
|
||||||
@@ -1,53 +1,48 @@
|
|||||||
#!/usr/bin/with-contenv bashio
|
#!/usr/bin/with-contenv bashio
|
||||||
# shellcheck shell=bash
|
# shellcheck shell=bash
|
||||||
# shellcheck disable=SC2015
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
# The image is Debian-based (apt) and always ships uv, so those are the only installers used.
|
||||||
if bashio::config.has_value 'additional_apps'; then
|
if bashio::config.has_value 'additional_apps'; then
|
||||||
bashio::log.info "Installing additional apps :"
|
bashio::log.info "Installing additional apps :"
|
||||||
NEWAPPS=$(bashio::config 'additional_apps')
|
apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null || bashio::log.warning "Unable to update apt package lists"
|
||||||
if command -v "apt-get" &> /dev/null; then
|
for packagestoinstall in $(bashio::config 'additional_apps' | tr ',' ' '); do
|
||||||
apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null || bashio::log.warning "Unable to update apt package lists"
|
|
||||||
fi
|
|
||||||
for packagestoinstall in ${NEWAPPS//,/ }; do
|
|
||||||
bashio::log.green "... $packagestoinstall"
|
bashio::log.green "... $packagestoinstall"
|
||||||
if command -v "apk" &> /dev/null; then
|
apt-get install -yqq --no-install-recommends "$packagestoinstall" &> /dev/null || bashio::log.fatal "Error : $packagestoinstall not found"
|
||||||
apk add --no-cache "$packagestoinstall" &> /dev/null || (bashio::log.fatal "Error : $packagestoinstall not found")
|
|
||||||
elif command -v "apt-get" &> /dev/null; then
|
|
||||||
apt-get install -yqq --no-install-recommends "$packagestoinstall" &> /dev/null || (bashio::log.fatal "Error : $packagestoinstall not found")
|
|
||||||
elif command -v "pacman" &> /dev/null; then
|
|
||||||
pacman --noconfirm -S "$packagestoinstall" &> /dev/null || (bashio::log.fatal "Error : $packagestoinstall not found")
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if bashio::config.has_value 'additional_pip'; then
|
if bashio::config.has_value 'additional_pip'; then
|
||||||
for p in $(bashio::config 'additional_pip' | tr ',' ' '); do
|
for p in $(bashio::config 'additional_pip' | tr ',' ' '); do
|
||||||
bashio::log.green "... pip: $p"
|
bashio::log.green "... pip: $p"
|
||||||
pip3 install --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed"
|
uv pip install --system --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if bashio::config.has_value 'TZ'; then
|
if bashio::config.has_value 'TZ'; then
|
||||||
TIMEZONE=$(bashio::config 'TZ')
|
TIMEZONE=$(bashio::config 'TZ')
|
||||||
bashio::log.info "Setting timezone to $TIMEZONE"
|
if [ -f "/usr/share/zoneinfo/$TIMEZONE" ]; then
|
||||||
ln -snf /usr/share/zoneinfo/"$TIMEZONE" /etc/localtime
|
bashio::log.info "Setting timezone to $TIMEZONE"
|
||||||
echo "$TIMEZONE" > /etc/timezone
|
ln -snf "/usr/share/zoneinfo/$TIMEZONE" /etc/localtime
|
||||||
fi || (bashio::log.fatal "Error : $TIMEZONE not found. Here is a list of valid timezones : https://manpages.ubuntu.com/manpages/focal/man3/DateTime::TimeZone::Catalog.3pm.html")
|
echo "$TIMEZONE" > /etc/timezone
|
||||||
|
else
|
||||||
|
bashio::log.fatal "Error : $TIMEZONE not found. Here is a list of valid timezones : https://manpages.ubuntu.com/manpages/focal/man3/DateTime::TimeZone::Catalog.3pm.html"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if bashio::config.has_value 'KEYBOARD'; then
|
if bashio::config.has_value 'KEYBOARD'; then
|
||||||
KEYBOARD=$(bashio::config 'KEYBOARD')
|
KEYBOARD=$(bashio::config 'KEYBOARD')
|
||||||
bashio::log.info "Setting keyboard to $KEYBOARD"
|
bashio::log.info "Setting keyboard to $KEYBOARD"
|
||||||
if [ -d /var/run/s6/container_environment ]; then printf "%s" "$KEYBOARD" > /var/run/s6/container_environment/KEYBOARD; fi
|
if [ -d /var/run/s6/container_environment ]; then printf "%s" "$KEYBOARD" > /var/run/s6/container_environment/KEYBOARD; fi
|
||||||
grep -qxF "KEYBOARD=\"$KEYBOARD\"" ~/.bashrc 2>/dev/null || printf "%s\n" "KEYBOARD=\"$KEYBOARD\"" >> ~/.bashrc
|
grep -qxF "KEYBOARD=\"$KEYBOARD\"" ~/.bashrc 2> /dev/null || printf "%s\n" "KEYBOARD=\"$KEYBOARD\"" >> ~/.bashrc
|
||||||
fi || true
|
fi
|
||||||
|
|
||||||
if bashio::config.has_value 'PASSWORD'; then
|
if bashio::config.has_value 'PASSWORD'; then
|
||||||
bashio::log.info "Setting password to the value defined in options"
|
bashio::log.info "Setting password to the value defined in options"
|
||||||
PASSWORD=$(bashio::config 'PASSWORD')
|
PASSWORD=$(bashio::config 'PASSWORD')
|
||||||
passwd -d abc
|
passwd -d abc
|
||||||
echo -e "$PASSWORD\n$PASSWORD" | passwd abc
|
echo -e "$PASSWORD\n$PASSWORD" | passwd abc
|
||||||
elif ! bashio::config.has_value 'PASSWORD' && { [[ -n "$(bashio::addon.port "3000")" ]] || [[ -n "$(bashio::addon.port "3001")" ]] }; then
|
elif [[ -n "$(bashio::addon.port "3000")" ]] || [[ -n "$(bashio::addon.port "3001")" ]]; then
|
||||||
bashio::log.warning "SEVERE RISK IDENTIFIED"
|
bashio::log.warning "SEVERE RISK IDENTIFIED"
|
||||||
bashio::log.warning "You are opening an external port but your password is not defined"
|
bashio::log.warning "You are opening an external port but your password is not defined"
|
||||||
bashio::log.warning "You risk being hacked ! Please disable the external ports, or use a password"
|
bashio::log.warning "You risk being hacked ! Please disable the external ports, or use a password"
|
||||||
|
|||||||
@@ -2,11 +2,9 @@
|
|||||||
# shellcheck shell=bash
|
# shellcheck shell=bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
if bashio::config.true 'auto_update'; then
|
bashio::log.info "Checking for Claude Desktop updates..."
|
||||||
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
|
||||||
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)"
|
||||||
bashio::log.info "Claude Desktop version: $(dpkg-query -W -f='${Version}' claude-desktop)"
|
else
|
||||||
else
|
bashio::log.warning "Update check failed (offline?), keeping current version"
|
||||||
bashio::log.warning "Update check failed (offline?), keeping current version"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -3,80 +3,414 @@
|
|||||||
set -e
|
set -e
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|
||||||
PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else echo '0'; fi)"
|
|
||||||
PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '0'; fi)"
|
|
||||||
mkdir -p "$HOME/.claude"
|
mkdir -p "$HOME/.claude"
|
||||||
|
CLAUDE_MD="$HOME/.claude/CLAUDE.md"
|
||||||
|
|
||||||
CLAUDE_DESKTOP_COMMAND_FILE="/tmp/claude-desktop-command"
|
run_as_runtime_user() {
|
||||||
DEFAULT_CLAUDE_DESKTOP_COMMAND='claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret'
|
s6-setuidgid abc env HOME="$HOME" "$@"
|
||||||
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
|
# Managed, idempotent guidance block in the user's global CLAUDE.md, delimited by
|
||||||
# Electron app force-overrides to the production endpoint (headroom #869), so transparent
|
# "<!-- BEGIN/END <name> (managed by claude_desktop addon) -->" markers. `add` appends the
|
||||||
# compression cannot be applied to the desktop launch. The integration that does work with
|
# block (body on stdin) unless the marker is already present; `remove` strips the whole
|
||||||
# Claude Desktop is headroom's MCP server, which exposes the headroom_compress/headroom_retrieve/
|
# block, surrounding blank padding included, and leaves everything else untouched.
|
||||||
# headroom_stats tools inside the app. Register it in Claude Desktop's MCP config, leaving the
|
manage_claude_md_block() {
|
||||||
# plain launch untouched. The merge is idempotent and preserves any other MCP servers.
|
local name="$1" action="$2"
|
||||||
CLAUDE_DESKTOP_CONFIG="$HOME/.config/Claude/claude_desktop_config.json"
|
local begin="<!-- BEGIN ${name} (managed by claude_desktop addon) -->"
|
||||||
if bashio::config.true 'install_headroom'; then
|
if [ "$action" = "add" ]; then
|
||||||
if command -v headroom &> /dev/null; then
|
if ! { [ -f "$CLAUDE_MD" ] && grep -qF "$begin" "$CLAUDE_MD"; }; then
|
||||||
bashio::log.info "headroom $(headroom --version 2> /dev/null || true) available; registering the headroom MCP server for Claude Desktop"
|
bashio::log.info "Adding ${name} guidance to CLAUDE.md"
|
||||||
HEADROOM_BIN="$(command -v headroom)" CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" python3 - <<'PY' || bashio::log.warning "Unable to register the headroom MCP server automatically"
|
mkdir -p "$(dirname "$CLAUDE_MD")"
|
||||||
|
{
|
||||||
|
[ -s "$CLAUDE_MD" ] && printf '\n'
|
||||||
|
printf '%s\n' "$begin"
|
||||||
|
cat
|
||||||
|
printf '%s\n' "<!-- END ${name} (managed by claude_desktop addon) -->"
|
||||||
|
} >> "$CLAUDE_MD"
|
||||||
|
fi
|
||||||
|
elif [ -f "$CLAUDE_MD" ] && grep -qF "$begin" "$CLAUDE_MD"; then
|
||||||
|
bashio::log.info "Removing ${name} guidance from CLAUDE.md"
|
||||||
|
CLAUDE_MD="$CLAUDE_MD" BLOCK_NAME="$name" python3 - <<'PY' || bashio::log.warning "Unable to remove the ${name} guidance automatically"
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
path = Path(os.environ["CLAUDE_MD"])
|
||||||
|
name = re.escape(os.environ["BLOCK_NAME"])
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
pattern = re.compile(
|
||||||
|
rf"\n*<!-- BEGIN {name} \(managed by claude_desktop addon\) -->.*?"
|
||||||
|
rf"<!-- END {name} \(managed by claude_desktop addon\) -->\n?",
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
new = pattern.sub("", text)
|
||||||
|
if new != text:
|
||||||
|
path.write_text(new, encoding="utf-8")
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Managed hook entry in ~/.claude/settings.json (settings hooks apply to terminal, cowork,
|
||||||
|
# dispatch and cron sessions alike). The managed command is stripped everywhere first and
|
||||||
|
# re-appended when adding, so one pass handles removal, de-duplication, and matcher migration
|
||||||
|
# on upgrades; hooks owned by other tools (e.g. tokensave's own entries) are preserved, and
|
||||||
|
# the final text comparison keeps the write idempotent across boots.
|
||||||
|
manage_settings_hook() {
|
||||||
|
# manage_settings_hook <event> <matcher> <command> <add|remove>
|
||||||
|
HOOK_EVENT="$1" HOOK_MATCHER="$2" HOOK_COMMAND="$3" HOOK_ACTION="$4" \
|
||||||
|
python3 - <<'PY' || bashio::log.warning "Unable to update the $1 hook for '$3'"
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
path = Path(os.environ["CLAUDE_DESKTOP_CONFIG"])
|
event = os.environ["HOOK_EVENT"]
|
||||||
|
matcher = os.environ["HOOK_MATCHER"]
|
||||||
|
command = os.environ["HOOK_COMMAND"]
|
||||||
|
action = os.environ["HOOK_ACTION"]
|
||||||
|
|
||||||
|
path = Path.home() / ".claude" / "settings.json"
|
||||||
|
original = path.read_text() if path.exists() else None
|
||||||
|
if original is None and action != "add":
|
||||||
|
raise SystemExit(0)
|
||||||
try:
|
try:
|
||||||
data = json.loads(path.read_text()) if path.exists() else {}
|
data = json.loads(original) if original is not None else {}
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
data = {}
|
data = {}
|
||||||
except Exception:
|
except Exception:
|
||||||
if path.exists():
|
if action != "add":
|
||||||
path.rename(path.with_suffix(path.suffix + ".bak"))
|
raise SystemExit(0)
|
||||||
|
path.rename(path.with_suffix(path.suffix + ".bak"))
|
||||||
|
original = None
|
||||||
data = {}
|
data = {}
|
||||||
servers = data.get("mcpServers")
|
|
||||||
if not isinstance(servers, dict):
|
hooks = data.get("hooks") if isinstance(data.get("hooks"), dict) else {}
|
||||||
servers = {}
|
entries = hooks.get(event) if isinstance(hooks.get(event), list) else []
|
||||||
data["mcpServers"] = servers
|
|
||||||
servers["headroom"] = {"command": os.environ.get("HEADROOM_BIN", "headroom"), "args": ["mcp", "serve"]}
|
filtered = []
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
for entry in entries:
|
||||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
if not isinstance(entry, dict) or not isinstance(entry.get("hooks"), list):
|
||||||
|
filtered.append(entry)
|
||||||
|
continue
|
||||||
|
kept = [
|
||||||
|
item
|
||||||
|
for item in entry["hooks"]
|
||||||
|
if not (isinstance(item, dict) and item.get("command") == command)
|
||||||
|
]
|
||||||
|
if len(kept) != len(entry["hooks"]):
|
||||||
|
if not kept:
|
||||||
|
continue
|
||||||
|
entry = dict(entry)
|
||||||
|
entry["hooks"] = kept
|
||||||
|
filtered.append(entry)
|
||||||
|
entries = filtered
|
||||||
|
|
||||||
|
if action == "add":
|
||||||
|
entries.append({"matcher": matcher, "hooks": [{"type": "command", "command": command}]})
|
||||||
|
|
||||||
|
if entries:
|
||||||
|
hooks[event] = entries
|
||||||
|
else:
|
||||||
|
hooks.pop(event, None)
|
||||||
|
if hooks:
|
||||||
|
data["hooks"] = hooks
|
||||||
|
else:
|
||||||
|
data.pop("hooks", None)
|
||||||
|
|
||||||
|
serialized = json.dumps(data, indent=2) + "\n"
|
||||||
|
if serialized != original:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(serialized)
|
||||||
PY
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
# Headroom's proxy routing works by setting ANTHROPIC_BASE_URL, which the Claude Desktop
|
||||||
|
# Electron app force-overrides to the production endpoint (headroom #869). Desktop therefore
|
||||||
|
# uses Headroom's MCP tools. Claude Code launches that resolve `claude` through PATH use the
|
||||||
|
# add-on's /usr/local/bin/claude wrapper and can be transparently proxied when enabled.
|
||||||
|
#
|
||||||
|
# Register the add-on-managed MCP servers (headroom, tokensave, homeassistant) in both Claude
|
||||||
|
# Desktop's config and Claude Code's user config (used by Desktop cowork/dispatch sessions).
|
||||||
|
# The merge is idempotent, preserves any other MCP servers, never overwrites a user-customized
|
||||||
|
# entry with a different command, and removes only add-on-managed entries when disabled.
|
||||||
|
CLAUDE_DESKTOP_CONFIG="$HOME/.config/Claude/claude_desktop_config.json"
|
||||||
|
CLAUDE_CODE_CONFIG="$HOME/.claude.json"
|
||||||
|
|
||||||
|
HEADROOM_ENABLED=false
|
||||||
|
if bashio::config.true 'install_headroom'; then
|
||||||
|
if command -v headroom &> /dev/null; then
|
||||||
|
HEADROOM_ENABLED=true
|
||||||
|
bashio::log.info "headroom $(headroom --version 2> /dev/null || true) available; registering the headroom MCP server"
|
||||||
else
|
else
|
||||||
bashio::log.warning "headroom is not available"
|
bashio::log.warning "headroom is not available"
|
||||||
fi
|
fi
|
||||||
elif [ -f "$CLAUDE_DESKTOP_CONFIG" ]; then
|
fi
|
||||||
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"
|
TOKENSAVE_ENABLED=false
|
||||||
|
if bashio::config.true 'install_tokensave'; then
|
||||||
|
if command -v tokensave &> /dev/null; then
|
||||||
|
TOKENSAVE_ENABLED=true
|
||||||
|
bashio::log.info "tokensave $(tokensave --version 2> /dev/null || true) available; configuring the complete Claude Code integration"
|
||||||
|
# The upstream installer adds the MCP entry, PreToolUse/UserPromptSubmit/Stop hooks,
|
||||||
|
# MCP permissions, global CLAUDE.md rules, and the global post-commit/checkout sync hook.
|
||||||
|
run_as_runtime_user tokensave install --agent claude --git-hook yes \
|
||||||
|
|| bashio::log.warning "tokensave Claude Code integration setup failed"
|
||||||
|
else
|
||||||
|
bashio::log.warning "tokensave is not available"
|
||||||
|
fi
|
||||||
|
elif command -v tokensave &> /dev/null; then
|
||||||
|
bashio::log.info "Removing the tokensave Claude Code integration"
|
||||||
|
run_as_runtime_user tokensave uninstall --agent claude \
|
||||||
|
|| bashio::log.warning "tokensave Claude Code integration removal failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initialize or incrementally sync only explicitly configured repositories. TokenSave deliberately
|
||||||
|
# requires one-time per-project opt-in; an empty list therefore has no startup or storage cost.
|
||||||
|
# Runs before the MCP registration merge below on purpose: a first-time `tokensave init` also
|
||||||
|
# rewrites ~/.claude.json itself (at default permissions), and the merge afterwards reconciles
|
||||||
|
# the managed entries and re-tightens the file mode around the stored HA token.
|
||||||
|
if $TOKENSAVE_ENABLED; then
|
||||||
|
declare -A TOKENSAVE_REPOS_SEEN=()
|
||||||
|
# Capture the list BEFORE looping: bashio::config's internals trip the errexit that
|
||||||
|
# process substitution inherits from the bashio wrapper (a `read -d ''` that always
|
||||||
|
# returns non-zero), so `done < <(bashio::config ...)` silently fed the loop an EMPTY
|
||||||
|
# list — the startup index/sync never ran. Command substitution runs without errexit
|
||||||
|
# (inherit_errexit is off), making this form reliable. bashio::config prints list
|
||||||
|
# entries one per line, without a trailing newline and as "null" when the key is absent
|
||||||
|
# (bashio::config.array only exists in the repo's standalone bashio, not the real one
|
||||||
|
# here); the `|| [ -n ... ]` test keeps the final unterminated record in the loop.
|
||||||
|
TOKENSAVE_PROJECT_PATHS="$(bashio::config 'tokensave_project_paths')"
|
||||||
|
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
|
||||||
|
# Trim surrounding whitespace while preserving spaces inside paths.
|
||||||
|
configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}"
|
||||||
|
configured_path="${configured_path%"${configured_path##*[![:space:]]}"}"
|
||||||
|
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$configured_path" in
|
||||||
|
/*) ;;
|
||||||
|
*)
|
||||||
|
bashio::log.warning "Skipping non-absolute tokensave_project_paths entry: ${configured_path}"
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [ ! -d "$configured_path" ]; then
|
||||||
|
bashio::log.warning "Skipping missing TokenSave project path: ${configured_path}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The one-shot safe.directory override is used only to discover the repository root.
|
||||||
|
repo_root="$(run_as_runtime_user git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
|
||||||
|
if [ -z "$repo_root" ] || [ "$repo_root" = "/" ]; then
|
||||||
|
bashio::log.warning "Skipping TokenSave path that is not a supported Git repository: ${configured_path}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ -n "${TOKENSAVE_REPOS_SEEN[$repo_root]:-}" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
TOKENSAVE_REPOS_SEEN[$repo_root]=1
|
||||||
|
|
||||||
|
# Persist the resolved root in the runtime user's Git config so the sync/init below,
|
||||||
|
# tokensave's git hooks, and Claude sessions all pass Git's dubious-ownership check.
|
||||||
|
if ! run_as_runtime_user git config --global --get-all safe.directory \
|
||||||
|
| grep -Fxq -- "$repo_root"; then
|
||||||
|
run_as_runtime_user git config --global --add safe.directory "$repo_root"
|
||||||
|
bashio::log.info "Marked TokenSave repository as safe for Git: ${repo_root}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
bashio::log.info "Preparing TokenSave index: ${repo_root}"
|
||||||
|
# Prepare the per-repo semantic graph defensively so a hard add-on stop or storage
|
||||||
|
# hiccup can never leave a broken index that fails every subsequent boot:
|
||||||
|
# * a startup-scoped flock serializes against an overlapping restart (and any git
|
||||||
|
# post-commit/checkout sync hook that fires mid-boot); waits up to 60s for the
|
||||||
|
# other writer to finish rather than silently skipping, since a held lock clears
|
||||||
|
# itself the moment its holder exits or dies (the kernel releases flock on exit);
|
||||||
|
# * an existing index is refreshed with a cheap incremental `sync`, retried a few
|
||||||
|
# times because SQLITE_BUSY under lock contention is transient, not corruption;
|
||||||
|
# * quarantine is reserved for sync failures whose stderr actually names database
|
||||||
|
# corruption (SQLite's own "malformed"/"not a database"/"disk image" wording) or
|
||||||
|
# a half-written index from an interrupted `init` (sentinel-flagged). Any other
|
||||||
|
# failure (permissions, disk full, missing binary, ...) leaves the existing index
|
||||||
|
# untouched and simply retries on the next start — corruption should self-heal,
|
||||||
|
# a transient environment problem should not nuke a healthy graph;
|
||||||
|
# * `init` is bracketed by a sentinel file so an interrupted full build is detected
|
||||||
|
# as incomplete on the next start and rebuilt rather than trusted.
|
||||||
|
# All file operations run as the abc runtime user because the repo `.tokensave`
|
||||||
|
# directory is not covered by the startup ownership pass.
|
||||||
|
# shellcheck disable=SC2016 # single-quoted on purpose: $1/$db/etc. expand in the abc shell
|
||||||
|
run_as_runtime_user bash -c '
|
||||||
|
set -o pipefail
|
||||||
|
repo_root="$1"
|
||||||
|
ts_dir="$repo_root/.tokensave"
|
||||||
|
db="$ts_dir/tokensave.db"
|
||||||
|
lock="$ts_dir/.startup.lock"
|
||||||
|
initflag="$ts_dir/.init-incomplete"
|
||||||
|
mkdir -p "$ts_dir"
|
||||||
|
exec 9>"$lock"
|
||||||
|
if ! flock -w 60 9; then
|
||||||
|
echo "TokenSave: index still locked for $repo_root after 60s; skipping startup sync" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
is_corruption() {
|
||||||
|
printf "%s" "$1" | grep -qiE "malformed|not a database|file is encrypted|disk image|database.*corrupt"
|
||||||
|
}
|
||||||
|
quarantine() {
|
||||||
|
stamp="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
bdir="$ts_dir/corrupt-$stamp"
|
||||||
|
mkdir -p "$bdir"
|
||||||
|
for f in "$db" "$db-wal" "$db-shm"; do
|
||||||
|
[ -e "$f" ] && mv -f "$f" "$bdir/" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
echo "TokenSave: quarantined suspect index to $bdir" >&2
|
||||||
|
}
|
||||||
|
if [ -f "$db" ] && [ ! -f "$initflag" ]; then
|
||||||
|
attempt=1
|
||||||
|
while :; do
|
||||||
|
sync_err="$(tokensave sync "$repo_root" 2>&1 1>/dev/null)" && exit 0
|
||||||
|
[ "$attempt" -ge 3 ] && break
|
||||||
|
echo "TokenSave: sync attempt $attempt failed for $repo_root; retrying" >&2
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
if is_corruption "$sync_err"; then
|
||||||
|
echo "TokenSave: sync failed after retries for $repo_root (corruption detected); rebuilding index" >&2
|
||||||
|
quarantine
|
||||||
|
else
|
||||||
|
echo "TokenSave: sync failed after retries for $repo_root (no corruption signature); leaving index in place, will retry next start" >&2
|
||||||
|
echo "TokenSave: last sync error: $sync_err" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif [ -f "$db" ]; then
|
||||||
|
echo "TokenSave: previous init did not finish for $repo_root; rebuilding index" >&2
|
||||||
|
quarantine
|
||||||
|
fi
|
||||||
|
: > "$initflag"
|
||||||
|
tokensave init "$repo_root" && { rm -f "$initflag"; exit 0; }
|
||||||
|
echo "TokenSave: init failed for $repo_root; will retry on next start" >&2
|
||||||
|
exit 1
|
||||||
|
' _ "$repo_root" \
|
||||||
|
|| bashio::log.warning "TokenSave preparation failed for ${repo_root}"
|
||||||
|
done <<< "$TOKENSAVE_PROJECT_PATHS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
HA_MCP_ENABLED=false
|
||||||
|
HA_MCP_URL=""
|
||||||
|
HA_MCP_TOKEN=""
|
||||||
|
if bashio::config.true 'enable_ha_mcp'; then
|
||||||
|
HA_MCP_URL="$(bashio::config 'ha_mcp_url' 'http://homeassistant:8123/api/mcp')"
|
||||||
|
if bashio::config.has_value 'ha_mcp_token'; then
|
||||||
|
HA_MCP_TOKEN="$(bashio::config 'ha_mcp_token')"
|
||||||
|
fi
|
||||||
|
if [ -z "$HA_MCP_TOKEN" ]; then
|
||||||
|
bashio::log.warning "enable_ha_mcp is on but ha_mcp_token is empty; set a Home Assistant long-lived access token (Profile -> Security) and enable the 'Model Context Protocol Server' integration"
|
||||||
|
elif ! command -v mcp-proxy &> /dev/null; then
|
||||||
|
bashio::log.warning "mcp-proxy is not available; cannot register the Home Assistant MCP server"
|
||||||
|
else
|
||||||
|
HA_MCP_ENABLED=true
|
||||||
|
bashio::log.info "Registering the Home Assistant MCP server (${HA_MCP_URL})"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
|
||||||
|
HEADROOM_HF_HOME="${HOME}/.headroom/hf" \
|
||||||
|
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \
|
||||||
|
HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \
|
||||||
|
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
|
||||||
|
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
|
||||||
|
python3 - <<'PY' || bashio::log.warning "Unable to update the MCP server registrations automatically"
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
path = Path(os.environ["CLAUDE_DESKTOP_CONFIG"])
|
MANAGED_BASENAMES = {
|
||||||
data = json.loads(path.read_text())
|
"headroom": "headroom",
|
||||||
if isinstance(data, dict):
|
"tokensave": "tokensave",
|
||||||
servers = data.get("mcpServers")
|
"homeassistant": "mcp-proxy",
|
||||||
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")
|
|
||||||
PY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Guide Claude to actually use the headroom compression tools so the MCP integration produces
|
desired = {}
|
||||||
# real savings (otherwise the tools sit unused and `headroom savings` stays empty). Managed,
|
if os.environ["HEADROOM_ENABLED"] == "true":
|
||||||
# idempotent block appended to the user's global CLAUDE.md; removed when headroom is disabled.
|
desired["headroom"] = {
|
||||||
CLAUDE_MD="$HOME/.claude/CLAUDE.md"
|
"command": os.environ["HEADROOM_BIN"],
|
||||||
HEADROOM_GUIDE_BEGIN="<!-- BEGIN headroom (managed by claude_desktop addon) -->"
|
"args": ["mcp", "serve", "--proxy-url", "http://127.0.0.1:8787"],
|
||||||
if bashio::config.true 'install_headroom'; then
|
# The MCP server is a separate process from the svc-headroom proxy longrun and does
|
||||||
mkdir -p "$(dirname "$CLAUDE_MD")"
|
# not inherit its HF_HOME export, so Kompress falls back to the default (tmpfs, wiped
|
||||||
if ! { [ -f "$CLAUDE_MD" ] && grep -qF "$HEADROOM_GUIDE_BEGIN" "$CLAUDE_MD"; }; then
|
# every restart) cache dir, never finds the model, and silently no-ops every
|
||||||
bashio::log.info "Adding headroom usage guidance to CLAUDE.md"
|
# compression request. Point it at the same persistent cache the proxy warms.
|
||||||
{
|
"env": {"HF_HOME": os.environ["HEADROOM_HF_HOME"]},
|
||||||
[ -s "$CLAUDE_MD" ] && printf '\n'
|
}
|
||||||
cat <<'MD'
|
if os.environ["TOKENSAVE_ENABLED"] == "true":
|
||||||
<!-- BEGIN headroom (managed by claude_desktop addon) -->
|
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 not isinstance(servers, dict):
|
||||||
|
servers = {}
|
||||||
|
changed = False
|
||||||
|
for name in MANAGED_BASENAMES:
|
||||||
|
existing = servers.get(name)
|
||||||
|
if name in desired:
|
||||||
|
entry = dict(desired[name])
|
||||||
|
if stdio_type:
|
||||||
|
entry["type"] = "stdio"
|
||||||
|
if existing is None or is_managed(name, existing):
|
||||||
|
if existing != entry:
|
||||||
|
servers[name] = entry
|
||||||
|
changed = True
|
||||||
|
elif existing is not None and is_managed(name, existing):
|
||||||
|
del servers[name]
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
if servers:
|
||||||
|
data["mcpServers"] = servers
|
||||||
|
else:
|
||||||
|
data.pop("mcpServers", None)
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(json.dumps(data, indent=2) + "\n")
|
||||||
|
# The Home Assistant long-lived access token is stored here in clear text. Enforced even
|
||||||
|
# on no-change boots because tokensave's own writes can recreate the file with default
|
||||||
|
# permissions between merges.
|
||||||
|
if path.exists():
|
||||||
|
path.chmod(0o600)
|
||||||
|
PY
|
||||||
|
|
||||||
|
# Guide Claude to actually use the Headroom compression tools so the MCP integration produces
|
||||||
|
# real savings when transparent proxying is unavailable.
|
||||||
|
if $HEADROOM_ENABLED; then
|
||||||
|
manage_claude_md_block headroom add <<'MD'
|
||||||
## Headroom context compression
|
## Headroom context compression
|
||||||
|
|
||||||
A local Headroom proxy (127.0.0.1:8787) backs the `headroom` MCP tools. To save context tokens:
|
A local Headroom proxy (127.0.0.1:8787) backs the `headroom` MCP tools. To save context tokens:
|
||||||
@@ -86,40 +420,32 @@ search results, JSON/config dumps, big command outputs, roughly >500 tokens —
|
|||||||
the raw content. Call `mcp__headroom__headroom_retrieve` with that hash when you need the full
|
the raw content. Call `mcp__headroom__headroom_retrieve` with that hash when you need the full
|
||||||
original back. Skip compression for error/stack-trace output (Headroom deliberately protects it)
|
original back. Skip compression for error/stack-trace output (Headroom deliberately protects it)
|
||||||
and for small or one-off content. Use `mcp__headroom__headroom_stats` to check savings.
|
and for small or one-off content. Use `mcp__headroom__headroom_stats` to check savings.
|
||||||
<!-- END headroom (managed by claude_desktop addon) -->
|
|
||||||
MD
|
MD
|
||||||
} >> "$CLAUDE_MD"
|
else
|
||||||
fi
|
manage_claude_md_block headroom remove
|
||||||
elif [ -f "$CLAUDE_MD" ] && grep -qF "$HEADROOM_GUIDE_BEGIN" "$CLAUDE_MD"; then
|
|
||||||
bashio::log.info "Removing headroom usage guidance from CLAUDE.md"
|
|
||||||
CLAUDE_MD="$CLAUDE_MD" python3 - <<'PY' || bashio::log.warning "Unable to remove headroom guidance automatically"
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
path = Path(os.environ["CLAUDE_MD"])
|
|
||||||
text = path.read_text()
|
|
||||||
pattern = re.compile(
|
|
||||||
r"\n*<!-- BEGIN headroom \(managed by claude_desktop addon\) -->.*?"
|
|
||||||
r"<!-- END headroom \(managed by claude_desktop addon\) -->\n?",
|
|
||||||
re.DOTALL,
|
|
||||||
)
|
|
||||||
new = pattern.sub("", text)
|
|
||||||
if new != text:
|
|
||||||
path.write_text(new)
|
|
||||||
PY
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if bashio::config.true 'install_rtk'; then
|
# Route every Claude Code session through the Headroom proxy via the `env` block in the user's
|
||||||
if command -v rtk &> /dev/null; then
|
# ~/.claude/settings.json. Claude Code writes settings `env` entries into the process
|
||||||
if [ -f "$HOME/.claude/settings.json" ] && grep -q 'rtk hook claude' "$HOME/.claude/settings.json"; then
|
# environment at startup, replacing inherited values — this is the only supported way to reach
|
||||||
bashio::log.info "rtk Claude Code hook already configured"
|
# Desktop cowork/local-agent-mode sessions, which spawn the bundled CLI at an absolute path
|
||||||
else
|
# (bypassing the PATH wrapper) with ANTHROPIC_BASE_URL pinned to the production endpoint
|
||||||
bashio::log.info "Configuring rtk Claude Code hook"
|
# (headroom #869). Managed-value semantics: only set or remove the variable when it is absent
|
||||||
RTK_NONINTERACTIVE=1 rtk init -g || bashio::log.warning "rtk global files configuration failed"
|
# or already equals the add-on-managed proxy URL, so a user-customized endpoint is never
|
||||||
python3 - <<'PY' || bashio::log.warning "Unable to configure rtk hook automatically"
|
# 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 json
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
MANAGED_URL = "http://127.0.0.1:8787"
|
||||||
|
|
||||||
path = Path.home() / ".claude" / "settings.json"
|
path = Path.home() / ".claude" / "settings.json"
|
||||||
try:
|
try:
|
||||||
data = json.loads(path.read_text()) if path.exists() else {}
|
data = json.loads(path.read_text()) if path.exists() else {}
|
||||||
@@ -129,79 +455,96 @@ except Exception:
|
|||||||
if path.exists():
|
if path.exists():
|
||||||
path.rename(path.with_suffix(path.suffix + ".bak"))
|
path.rename(path.with_suffix(path.suffix + ".bak"))
|
||||||
data = {}
|
data = {}
|
||||||
hooks = data.setdefault("hooks", {})
|
|
||||||
pre = hooks.setdefault("PreToolUse", [])
|
env = data.get("env")
|
||||||
rtk_entry = {"matcher": "Bash", "hooks": [{"type": "command", "command": "rtk hook claude"}]}
|
if not isinstance(env, dict):
|
||||||
if not any("rtk hook claude" in json.dumps(entry) for entry in pre if isinstance(entry, dict)):
|
env = {}
|
||||||
pre.append(rtk_entry)
|
current = env.get("ANTHROPIC_BASE_URL")
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
changed = False
|
||||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
|
||||||
|
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
|
PY
|
||||||
fi
|
|
||||||
|
# Compress large tool outputs automatically in every Claude Code session via a managed
|
||||||
|
# PostToolUse hook. Desktop-spawned sessions pin ANTHROPIC_BASE_URL to the production endpoint
|
||||||
|
# (headroom #869) so the proxy never sees their traffic, and the CLAUDE.md guidance above only
|
||||||
|
# helps when the model remembers to call the MCP tools. The hook closes that gap: outputs over
|
||||||
|
# ~4000 chars from Bash/Grep/Glob/WebFetch are compressed with Headroom's rule-based pipeline
|
||||||
|
# and swapped in through hookSpecificOutput.updatedToolOutput, with the original kept in the
|
||||||
|
# shared CCR store so the model can fetch it back with mcp__headroom__headroom_retrieve. The
|
||||||
|
# script fails open (any error leaves the tool output untouched) and its --self-test gate
|
||||||
|
# keeps a broken interpreter path from registering a hook that would warn on every tool call.
|
||||||
|
HEADROOM_HOOK_CMD="/usr/local/bin/headroom-posttooluse-compress.py"
|
||||||
|
HEADROOM_HOOK_ACTION="remove"
|
||||||
|
if $HEADROOM_ENABLED && bashio::config.true 'headroom_auto_compress'; then
|
||||||
|
if run_as_runtime_user "$HEADROOM_HOOK_CMD" --self-test; then
|
||||||
|
HEADROOM_HOOK_ACTION="add"
|
||||||
|
bashio::log.info "Registering the Headroom PostToolUse auto-compression hook"
|
||||||
|
else
|
||||||
|
bashio::log.warning "headroom-posttooluse-compress.py --self-test failed; not registering the auto-compression hook"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
manage_settings_hook PostToolUse "Bash|Grep|Glob|WebFetch" "$HEADROOM_HOOK_CMD" "$HEADROOM_HOOK_ACTION"
|
||||||
|
|
||||||
|
# Tell Claude Code that it can configure Home Assistant over the Core API via the shipped
|
||||||
|
# `ha-cli` helper (no /config filesystem mount needed).
|
||||||
|
if bashio::config.true 'enable_ha_api_helper'; then
|
||||||
|
manage_claude_md_block ha-api-helper add <<'MD'
|
||||||
|
## Configuring Home Assistant
|
||||||
|
|
||||||
|
You can configure this Home Assistant instance through its Core API using the `ha-cli`
|
||||||
|
command (on `PATH`). It authenticates automatically with the add-on's `$SUPERVISOR_TOKEN`,
|
||||||
|
so no token setup is needed. There is **no `/config` filesystem mount** — work only through
|
||||||
|
`ha-cli`, and never try to read or write Home Assistant YAML files directly.
|
||||||
|
|
||||||
|
What is editable this way: automations, scripts, and scenes
|
||||||
|
(`ha-cli get|post|delete config/automation/config/<id>` and the `script`/`scene` equivalents);
|
||||||
|
service calls (`ha-cli call <domain.service> '<json>'`); state reads (`ha-cli states`); and,
|
||||||
|
over WebSocket, helpers, dashboards, and area/label/floor/entity registries
|
||||||
|
(`ha-cli ws '{"type":"..."}'`). Run `ha-cli --help` for the full reference. Raw YAML
|
||||||
|
(`configuration.yaml`, `secrets.yaml`) is intentionally unreachable — if a change needs it,
|
||||||
|
say so instead of working around it.
|
||||||
|
|
||||||
|
Rules: run `ha-cli config` first to confirm connectivity; **read the current object and show
|
||||||
|
the user the intended change, then wait for confirmation** before any create/update/delete or
|
||||||
|
any state-changing `call`; after writing, read the object back and reload if needed
|
||||||
|
(e.g. `ha-cli call automation.reload`).
|
||||||
|
MD
|
||||||
|
else
|
||||||
|
manage_claude_md_block ha-api-helper remove
|
||||||
|
fi
|
||||||
|
|
||||||
|
if bashio::config.true 'install_rtk'; then
|
||||||
|
if command -v rtk &> /dev/null; then
|
||||||
|
bashio::log.info "Configuring rtk Claude Code integration"
|
||||||
|
# `rtk init -g` writes ~/.claude/RTK.md and its @RTK.md include in CLAUDE.md, but in
|
||||||
|
# non-interactive mode it deliberately refuses to patch settings.json, so the hook
|
||||||
|
# entry that actually rewrites Bash commands is registered here.
|
||||||
|
run_as_runtime_user env RTK_NONINTERACTIVE=1 rtk init -g \
|
||||||
|
|| bashio::log.warning "rtk global files configuration failed"
|
||||||
|
manage_settings_hook PreToolUse Bash "rtk hook claude" add
|
||||||
else
|
else
|
||||||
bashio::log.warning "rtk is not available"
|
bashio::log.warning "rtk is not available"
|
||||||
fi
|
fi
|
||||||
elif [ -f "$HOME/.claude/settings.json" ]; then
|
else
|
||||||
bashio::log.info "Removing the add-on-managed rtk Claude Code hook"
|
manage_settings_hook PreToolUse Bash "rtk hook claude" remove
|
||||||
python3 - <<'PY' || bashio::log.warning "Unable to remove rtk hook automatically"
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
path = Path.home() / ".claude" / "settings.json"
|
|
||||||
data = json.loads(path.read_text())
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
raise TypeError("Claude settings must contain a JSON object")
|
|
||||||
|
|
||||||
hooks = data.get("hooks")
|
|
||||||
if not isinstance(hooks, dict):
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
entries = hooks.get("PreToolUse")
|
|
||||||
if not isinstance(entries, list):
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
changed = False
|
|
||||||
filtered_entries = []
|
|
||||||
for entry in entries:
|
|
||||||
if not isinstance(entry, dict) or entry.get("matcher") != "Bash":
|
|
||||||
filtered_entries.append(entry)
|
|
||||||
continue
|
|
||||||
|
|
||||||
commands = entry.get("hooks")
|
|
||||||
if not isinstance(commands, list):
|
|
||||||
filtered_entries.append(entry)
|
|
||||||
continue
|
|
||||||
|
|
||||||
filtered_commands = [
|
|
||||||
command
|
|
||||||
for command in commands
|
|
||||||
if not (
|
|
||||||
isinstance(command, dict)
|
|
||||||
and command.get("type") == "command"
|
|
||||||
and command.get("command") == "rtk hook claude"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
if len(filtered_commands) == len(commands):
|
|
||||||
filtered_entries.append(entry)
|
|
||||||
continue
|
|
||||||
|
|
||||||
changed = True
|
|
||||||
if filtered_commands:
|
|
||||||
updated_entry = dict(entry)
|
|
||||||
updated_entry["hooks"] = filtered_commands
|
|
||||||
filtered_entries.append(updated_entry)
|
|
||||||
|
|
||||||
if changed:
|
|
||||||
if filtered_entries:
|
|
||||||
hooks["PreToolUse"] = filtered_entries
|
|
||||||
else:
|
|
||||||
hooks.pop("PreToolUse", None)
|
|
||||||
if hooks:
|
|
||||||
data["hooks"] = hooks
|
|
||||||
else:
|
|
||||||
data.pop("hooks", None)
|
|
||||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
|
||||||
PY
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if bashio::config.true 'install_caveman'; then
|
if bashio::config.true 'install_caveman'; then
|
||||||
@@ -209,17 +552,13 @@ if bashio::config.true 'install_caveman'; then
|
|||||||
bashio::log.info "caveman Claude Code plugin already configured"
|
bashio::log.info "caveman Claude Code plugin already configured"
|
||||||
else
|
else
|
||||||
bashio::log.info "Installing caveman Claude Code plugin"
|
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
|
fi
|
||||||
else
|
else
|
||||||
bashio::log.info "Disabling caveman Claude Code plugin"
|
bashio::log.info "Disabling caveman Claude Code plugin"
|
||||||
find "$HOME/.claude" -maxdepth 4 -iname '*caveman*' -exec rm -rf {} + 2> /dev/null || true
|
find "$HOME/.claude" -maxdepth 4 -iname '*caveman*' -exec rm -rf {} + 2> /dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Startup configuration runs as root, while Claude Desktop and the web terminal run as abc.
|
# Ownership of everything written above is reconciled by 84-claude_runtime_ownership.sh after
|
||||||
# Return managed persistent files to the configured runtime UID/GID after all writes complete.
|
# the remaining Claude configuration scripts have run.
|
||||||
for managed_path in "$HOME/.claude" "$HOME/.config/Claude"; do
|
|
||||||
if [ -e "$managed_path" ]; then
|
|
||||||
chown -R -- "${PUID}:${PGID}" "$managed_path" || bashio::log.warning "Unable to set ownership on $managed_path"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|||||||
73
claude_desktop/rootfs/etc/cont-init.d/83-claude_permissions.sh
Executable file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/with-contenv bashio
|
||||||
|
# shellcheck shell=bash
|
||||||
|
set -e
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
PERMISSION_MODE="$(bashio::config 'permission_mode')"
|
||||||
|
SETTINGS_PATH="$HOME/.claude/settings.json"
|
||||||
|
|
||||||
|
case "$PERMISSION_MODE" in
|
||||||
|
strict | auto | bypass) ;;
|
||||||
|
*)
|
||||||
|
bashio::log.warning "Unknown permission_mode '${PERMISSION_MODE}'; falling back to strict"
|
||||||
|
PERMISSION_MODE="strict"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Managed-value semantics, matching the ANTHROPIC_BASE_URL handling in 82-claude_tools.sh:
|
||||||
|
# auto/bypass set permissions.defaultMode to the add-on-managed value, and strict removes it
|
||||||
|
# only while it still holds one of those managed values — a defaultMode the user set by hand
|
||||||
|
# is never deleted. Ownership of the written file is reconciled by 84-claude_runtime_ownership.sh.
|
||||||
|
mkdir -p "$(dirname "$SETTINGS_PATH")"
|
||||||
|
PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
MANAGED_VALUES = {"auto", "bypassPermissions"}
|
||||||
|
|
||||||
|
mode = os.environ["PERMISSION_MODE"]
|
||||||
|
settings_path = Path(os.environ["SETTINGS_PATH"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
settings = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
if settings_path.exists():
|
||||||
|
settings_path.rename(settings_path.with_suffix(settings_path.suffix + ".bak"))
|
||||||
|
settings = {}
|
||||||
|
if not isinstance(settings, dict):
|
||||||
|
settings = {}
|
||||||
|
|
||||||
|
permissions = settings.get("permissions")
|
||||||
|
if not isinstance(permissions, dict):
|
||||||
|
permissions = {}
|
||||||
|
|
||||||
|
if mode == "strict":
|
||||||
|
if permissions.get("defaultMode") in MANAGED_VALUES:
|
||||||
|
permissions.pop("defaultMode")
|
||||||
|
else:
|
||||||
|
permissions["defaultMode"] = "auto" if mode == "auto" else "bypassPermissions"
|
||||||
|
|
||||||
|
if permissions:
|
||||||
|
settings["permissions"] = permissions
|
||||||
|
else:
|
||||||
|
settings.pop("permissions", None)
|
||||||
|
|
||||||
|
settings_path.write_text(json.dumps(settings, indent=2) + "\n")
|
||||||
|
settings_path.chmod(0o600)
|
||||||
|
PY
|
||||||
|
|
||||||
|
# Drop the state file older add-on versions used to remember the pre-add-on defaultMode.
|
||||||
|
rm -f "$HOME/.claude/.addon-permission-mode.json"
|
||||||
|
|
||||||
|
case "$PERMISSION_MODE" in
|
||||||
|
strict)
|
||||||
|
bashio::log.info "Claude Code permission mode: strict (normal prompts)"
|
||||||
|
;;
|
||||||
|
auto)
|
||||||
|
bashio::log.info "Claude Code permission mode: auto (safe actions approved automatically)"
|
||||||
|
;;
|
||||||
|
bypass)
|
||||||
|
bashio::log.warning "Claude Code permission mode: bypass (permission checks disabled for mounted data and available tools)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -17,25 +17,54 @@ if ! command -v gh > /dev/null 2>&1; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
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
|
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
|
fi
|
||||||
|
|
||||||
if bashio::config.has_value 'github_email'; then
|
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
|
fi
|
||||||
|
|
||||||
if bashio::config.has_value 'github_token'; then
|
if bashio::config.has_value 'github_token'; then
|
||||||
token="$(bashio::config 'github_token')"
|
token="$(bashio::config 'github_token')"
|
||||||
mkdir -p "$HOME/.config/gh"
|
run_as_runtime_user mkdir -p "$HOME/.config/gh"
|
||||||
chmod 700 "$HOME/.config/gh"
|
run_as_runtime_user chmod 700 "$HOME/.config/gh"
|
||||||
if env -u GH_TOKEN -u GITHUB_TOKEN gh auth status --hostname github.com > /dev/null 2>&1; then
|
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"
|
bashio::log.info "GitHub CLI already authenticated for github.com"
|
||||||
else
|
else
|
||||||
bashio::log.info "Configuring GitHub CLI authentication for github.com"
|
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
|
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
|
else
|
||||||
bashio::log.info "GitHub CLI available. Set github_token to authenticate gh and git operations."
|
bashio::log.info "GitHub CLI available. Set github_token to authenticate gh and git operations."
|
||||||
fi
|
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
|
||||||
@@ -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 port=8787
|
||||||
declare host=127.0.0.1
|
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}"
|
bashio::log.info "svc-headroom: starting local Headroom proxy on ${host}:${port}"
|
||||||
exec s6-setuidgid abc headroom proxy --host "${host}" --port "${port}" --code-aware
|
exec s6-setuidgid abc headroom proxy --host "${host}" --port "${port}" --code-aware
|
||||||
fi
|
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
|
#!/usr/bin/with-contenv bashio
|
||||||
# Hourly rtk + headroom token-savings snapshot for the add-on log.
|
# Hourly RTK + Headroom + TokenSave token-savings snapshot for the add-on log.
|
||||||
# Invoked by cron (see /defaults/crontabs/root); its stdout is redirected to /proc/1/fd/1,
|
# Invoked by cron (see /defaults/crontabs/root); 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
|
# Each tool is reported independently so enabling Headroom cannot hide RTK or TokenSave data.
|
||||||
# growing, the corresponding tool has stopped working.
|
# with-contenv supplies the configured persistent HOME.
|
||||||
# with-contenv supplies HOME from the s6 envdir, so this honors a custom `data_location`
|
export NO_COLOR=1
|
||||||
# (see 20-folders.sh) instead of hardcoding /data/data; it also makes bashio::config
|
|
||||||
# available for the install_headroom gate below.
|
|
||||||
export NO_COLOR=1 # keep the add-on log free of ANSI color codes
|
|
||||||
export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
|
export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
|
||||||
|
|
||||||
have_rtk=false; command -v rtk >/dev/null 2>&1 && have_rtk=true
|
if ! bashio::config.true 'enable_tools_health_report'; then
|
||||||
have_headroom=false; command -v headroom >/dev/null 2>&1 && have_headroom=true
|
exit 0
|
||||||
|
|
||||||
# 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
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Nothing to report if neither tool is active — stay quiet.
|
rtk_enabled=false
|
||||||
if ! $have_rtk && ! $headroom_enabled; then exit 0; fi
|
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
|
if $headroom_enabled; then
|
||||||
echo "--- headroom savings ---"
|
echo "--- headroom savings ---"
|
||||||
headroom savings 2>&1 || echo "[warn] headroom savings failed"
|
headroom savings 2>&1 || echo "[warn] headroom savings failed"
|
||||||
elif $have_rtk; then
|
fi
|
||||||
|
if $rtk_enabled; then
|
||||||
echo "--- rtk gain ---"
|
echo "--- rtk gain ---"
|
||||||
rtk gain 2>&1 || echo "[warn] rtk gain failed"
|
rtk gain 2>&1 || echo "[warn] rtk gain failed"
|
||||||
fi
|
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"
|
|
||||||
178
claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh
Executable file
@@ -0,0 +1,178 @@
|
|||||||
|
#!/usr/bin/with-contenv bashio
|
||||||
|
# Diagnose installation, registration, routing, indexing, permissions, and recorded savings without
|
||||||
|
# printing MCP environment values (which may contain the Home Assistant access token).
|
||||||
|
# shellcheck shell=bash
|
||||||
|
set +e
|
||||||
|
set -o pipefail
|
||||||
|
export NO_COLOR=1
|
||||||
|
export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
|
||||||
|
|
||||||
|
section() {
|
||||||
|
printf '\n=== %s ===\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
section "Installed binaries"
|
||||||
|
for tool in claude claude-desktop headroom rtk tokensave git gh rg jq shellcheck yamllint hadolint actionlint; do
|
||||||
|
resolved="$(command -v "$tool" 2> /dev/null || true)"
|
||||||
|
if [ -n "$resolved" ]; then
|
||||||
|
printf '%-16s %s\n' "$tool" "$resolved"
|
||||||
|
else
|
||||||
|
printf '%-16s %s\n' "$tool" "MISSING"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
section "Configured switches"
|
||||||
|
for option in permission_mode install_headroom headroom_wrap_claude_code expose_headroom_dashboard install_rtk install_tokensave install_caveman enable_tools_health_report; do
|
||||||
|
printf '%-30s %s\n' "$option" "$(bashio::config "$option")"
|
||||||
|
done
|
||||||
|
|
||||||
|
section "Runtime identity"
|
||||||
|
printf '%-30s %s\n' "configured PUID:PGID" "$(bashio::config 'PUID'):$(bashio::config 'PGID')"
|
||||||
|
printf '%-30s %s\n' "effective abc UID:GID" "$(id -u abc):$(id -g abc)"
|
||||||
|
printf '%-30s %s\n' "current process UID:GID" "$(id -u):$(id -g)"
|
||||||
|
if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
|
||||||
|
if [ "$(id -u abc)" -eq 0 ]; then
|
||||||
|
echo "bypass runtime: ERROR - Claude Code will reject bypass permissions while abc is root"
|
||||||
|
else
|
||||||
|
echo "bypass runtime: OK - Claude Desktop and Cowork run as a non-root UID"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "Claude Code permission state"
|
||||||
|
python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
path = Path.home() / ".claude/settings.json"
|
||||||
|
try:
|
||||||
|
data = json.loads(path.read_text())
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("settings: MISSING")
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"settings: INVALID: {exc}")
|
||||||
|
else:
|
||||||
|
permissions = data.get("permissions", {})
|
||||||
|
if isinstance(permissions, dict):
|
||||||
|
print(f"permissions.defaultMode: {permissions.get('defaultMode', '<upstream default>')}")
|
||||||
|
else:
|
||||||
|
print("permissions: INVALID")
|
||||||
|
PY
|
||||||
|
|
||||||
|
section "MCP registrations (environment values redacted)"
|
||||||
|
python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
paths = [
|
||||||
|
Path.home() / ".claude.json",
|
||||||
|
Path.home() / ".config/Claude/claude_desktop_config.json",
|
||||||
|
]
|
||||||
|
for path in paths:
|
||||||
|
print(path)
|
||||||
|
try:
|
||||||
|
data = json.loads(path.read_text())
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(" MISSING")
|
||||||
|
continue
|
||||||
|
except Exception as exc:
|
||||||
|
print(f" INVALID: {exc}")
|
||||||
|
continue
|
||||||
|
servers = data.get("mcpServers", {})
|
||||||
|
if not isinstance(servers, dict) or not servers:
|
||||||
|
print(" no MCP servers")
|
||||||
|
continue
|
||||||
|
for name, spec in sorted(servers.items()):
|
||||||
|
if not isinstance(spec, dict):
|
||||||
|
print(f" {name}: invalid entry")
|
||||||
|
continue
|
||||||
|
command = spec.get("command", "?")
|
||||||
|
args = spec.get("args", [])
|
||||||
|
server_type = spec.get("type", "")
|
||||||
|
suffix = f" type={server_type}" if server_type else ""
|
||||||
|
print(f" {name}: {command} {args}{suffix}")
|
||||||
|
if spec.get("env"):
|
||||||
|
print(" env: <redacted>")
|
||||||
|
PY
|
||||||
|
|
||||||
|
section "Claude Code hooks"
|
||||||
|
python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
path = Path.home() / ".claude/settings.json"
|
||||||
|
try:
|
||||||
|
data = json.loads(path.read_text())
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("MISSING")
|
||||||
|
raise SystemExit(0)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"INVALID: {exc}")
|
||||||
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
hooks = data.get("hooks", {})
|
||||||
|
if not isinstance(hooks, dict) or not hooks:
|
||||||
|
print("no hooks")
|
||||||
|
raise SystemExit(0)
|
||||||
|
for event, entries in hooks.items():
|
||||||
|
print(event)
|
||||||
|
if not isinstance(entries, list):
|
||||||
|
print(" invalid entries")
|
||||||
|
continue
|
||||||
|
for entry in entries:
|
||||||
|
matcher = entry.get("matcher", "*") if isinstance(entry, dict) else "?"
|
||||||
|
commands = entry.get("hooks", []) if isinstance(entry, dict) else []
|
||||||
|
rendered = []
|
||||||
|
for command in commands if isinstance(commands, list) else []:
|
||||||
|
if isinstance(command, dict):
|
||||||
|
rendered.append(" ".join([str(command.get("command", "?")), *map(str, command.get("args", []))]))
|
||||||
|
print(f" matcher={matcher}: {', '.join(rendered) or 'no command'}")
|
||||||
|
PY
|
||||||
|
|
||||||
|
section "Headroom"
|
||||||
|
if bashio::config.true 'install_headroom'; then
|
||||||
|
curl -fsS --max-time 2 http://127.0.0.1:8787/health && echo || echo "proxy health: FAILED"
|
||||||
|
headroom mcp status || true
|
||||||
|
headroom savings || true
|
||||||
|
else
|
||||||
|
echo "disabled"
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "RTK"
|
||||||
|
if bashio::config.true 'install_rtk'; then
|
||||||
|
rtk gain || true
|
||||||
|
else
|
||||||
|
echo "disabled"
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "TokenSave"
|
||||||
|
if bashio::config.true 'install_tokensave'; then
|
||||||
|
tokensave doctor --agent claude || true
|
||||||
|
tokensave gain --all --range 30d || true
|
||||||
|
# Capture before looping — see the matching comment in 82-claude_tools.sh: feeding the
|
||||||
|
# loop straight from `< <(bashio::config ...)` yields an empty list under errexit.
|
||||||
|
TOKENSAVE_PROJECT_PATHS="$(bashio::config 'tokensave_project_paths')"
|
||||||
|
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
|
||||||
|
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
repo_root="$(s6-setuidgid abc env HOME="$HOME" git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
|
||||||
|
if [ -z "$repo_root" ]; then
|
||||||
|
echo "${configured_path}: not a Git repository"
|
||||||
|
elif [ -f "$repo_root/.tokensave/tokensave.db" ]; then
|
||||||
|
s6-setuidgid abc env HOME="$HOME" tokensave status "$repo_root" --short || true
|
||||||
|
else
|
||||||
|
echo "${repo_root}: NOT INITIALIZED"
|
||||||
|
fi
|
||||||
|
done <<< "$TOKENSAVE_PROJECT_PATHS"
|
||||||
|
else
|
||||||
|
echo "disabled"
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "Claude routing"
|
||||||
|
printf 'PATH claude: %s\n' "$(command -v claude 2> /dev/null || true)"
|
||||||
|
printf 'real claude: %s\n' "$([ -x /usr/bin/claude ] && echo /usr/bin/claude || echo MISSING)"
|
||||||
|
if bashio::config.true 'headroom_wrap_claude_code'; then
|
||||||
|
echo "PATH-based Claude Code launches are configured for Headroom wrapping."
|
||||||
|
else
|
||||||
|
echo "Claude Code Headroom wrapping is disabled; Headroom remains available through MCP."
|
||||||
|
fi
|
||||||
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",
|
"upstream_repo": "linuxserver/docker-baseimage-selkies",
|
||||||
"github_fulltag": true,
|
"github_fulltag": true,
|
||||||
"slug": "claude_desktop",
|
"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)
|
## 2.9.14 (2026-06-20)
|
||||||
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)
|
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
#=== Home Assistant Addon ===#
|
#=== Home Assistant Addon ===#
|
||||||
|
|
||||||
# ARGs used in FROM must be declared before any FROM instruction
|
# 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 #
|
# 1 Build Image #
|
||||||
|
|||||||
@@ -91,5 +91,5 @@ schema:
|
|||||||
TZ: str?
|
TZ: str?
|
||||||
slug: cleanuparr
|
slug: cleanuparr
|
||||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/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]"
|
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",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "cleanuparr",
|
"slug": "cleanuparr",
|
||||||
"source": "github",
|
"source": "github",
|
||||||
"upstream_repo": "Cleanuparr/Cleanuparr",
|
"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,10 @@
|
|||||||
|
|
||||||
|
## 2.2.0 (2026-07-16)
|
||||||
|
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||||
|
|
||||||
|
## 2.1.2 (2026-07-11)
|
||||||
|
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||||
|
|
||||||
## 2.1.0 (2026-07-04)
|
## 2.1.0 (2026-07-04)
|
||||||
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
|
||||||
|
|
||||||
|
|||||||
@@ -101,4 +101,4 @@ schema:
|
|||||||
slug: codex
|
slug: codex
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
url: https://github.com/alexbelgium/hassio-addons
|
||||||
version: "2.1.0"
|
version: "2.2.0"
|
||||||
|
|||||||