mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-09-16 06:39:08 +02:00
Compare commits
92 Commits
feat/claud
...
fix/claude
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fc84fa6da | ||
|
|
0ee96086c3 | ||
|
|
462767d34e | ||
|
|
32765daf99 | ||
|
|
3d2f3aa193 | ||
|
|
26101a1104 | ||
|
|
a4764364da | ||
|
|
9bbd72e70a | ||
|
|
e345752d00 | ||
|
|
7e2c8eda5e | ||
|
|
65fbc7dae2 | ||
|
|
95641d253c | ||
|
|
bdc6231aa3 | ||
|
|
ae2c29b977 | ||
|
|
3114a6cc94 | ||
|
|
297102e908 | ||
|
|
39efd5602f | ||
|
|
8b3db6e325 | ||
|
|
e70b8db0fa | ||
|
|
4872bd89c9 | ||
|
|
b3c27024d8 | ||
|
|
b8c7cc3815 | ||
|
|
3354af026f | ||
|
|
3bff6b65de | ||
|
|
efc659f452 | ||
|
|
2f245c77e1 | ||
|
|
2347cb9eae | ||
|
|
0cfe28a405 | ||
|
|
26c26e3668 | ||
|
|
64c7f2dd5b | ||
|
|
259517289a | ||
|
|
b26da2e161 | ||
|
|
9ec6c44f03 | ||
|
|
ff43c4eeba | ||
|
|
a1e68ee807 | ||
|
|
2a1412d957 | ||
|
|
b328ae242f | ||
|
|
9c0521da49 | ||
|
|
7d2c6eb9b2 | ||
|
|
f990177df8 | ||
|
|
005275315a | ||
|
|
0223cc3511 | ||
|
|
e5983f4718 | ||
|
|
587121cb1b | ||
|
|
29f2cfd198 | ||
|
|
3329c4b40d | ||
|
|
9471dffcca | ||
|
|
215327e439 | ||
|
|
8ecbfccdcd | ||
|
|
5a2efc4f9e | ||
|
|
ef0aecbde6 | ||
|
|
7a6ad72617 | ||
|
|
b5985f230e | ||
|
|
d48d1f4d8d | ||
|
|
6bf379bffc | ||
|
|
5d577ad954 | ||
|
|
85dab2ae6e | ||
|
|
ad50abc365 | ||
|
|
dc280a5caf | ||
|
|
c0c1df8c27 | ||
|
|
a38bc75f95 | ||
|
|
8073317150 | ||
|
|
15505a265f | ||
|
|
d1ceafebe8 | ||
|
|
c76b257f5d | ||
|
|
321f2fde73 | ||
|
|
45f88307d4 | ||
|
|
7cd82b2758 | ||
|
|
66d3886b80 | ||
|
|
bdd56b8057 | ||
|
|
9c55193c38 | ||
|
|
6b6233e1fa | ||
|
|
22951ac4f5 | ||
|
|
006f3052ac | ||
|
|
0ff2e8f783 | ||
|
|
3c8a32199e | ||
|
|
c8d706c9de | ||
|
|
c04f3e288c | ||
|
|
f6c2bef0be | ||
|
|
f8c447ed19 | ||
|
|
bd82e72ffa | ||
|
|
a3ecb38fea | ||
|
|
eada7a0ba0 | ||
|
|
e7921b822e | ||
|
|
aa1486c1d8 | ||
|
|
b4008c4db9 | ||
|
|
fa2328f741 | ||
|
|
e8bb55682b | ||
|
|
7fdf95940b | ||
|
|
583c5e655a | ||
|
|
50150a4775 | ||
|
|
6814088369 |
3
.github/workflows/onpush_builder.yaml
vendored
3
.github/workflows/onpush_builder.yaml
vendored
@@ -300,7 +300,7 @@ jobs:
|
||||
|
||||
- name: Build ${{ matrix.addon }} add-on
|
||||
if: steps.info.outputs.build_arch == 'true' && steps.info.outputs.has_dockerfile == 'true'
|
||||
uses: home-assistant/builder/actions/build-image@2026.03.2
|
||||
uses: home-assistant/builder/actions/build-image@2026.06.0
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
cache-gha: "false"
|
||||
@@ -433,4 +433,3 @@ jobs:
|
||||
done
|
||||
|
||||
git push origin HEAD:master
|
||||
|
||||
|
||||
@@ -258,13 +258,15 @@ If you want to do add the repository manually, please follow the procedure highl
|
||||
![amd64][amd64-badge]
|
||||
![ingress][ingress-badge]
|
||||
|
||||
✓  [Claude Desktop](claude_desktop/) : Claude Desktop and a persistent Claude Code web terminal
|
||||
✓  [Claude Desktop](claude_desktop/) : Claude Desktop with Headroom, RTK, and TokenSave optimization
|
||||
|
||||
  
|
||||

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

|
||||
![aarch64][aarch64-badge]
|
||||
![amd64][amd64-badge]
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
## source-20260716 (16-07-2026)
|
||||
- Minor bugs fixed
|
||||
## source-20260714 (14-07-2026)
|
||||
- Minor bugs fixed
|
||||
## source-20260709 (09-07-2026)
|
||||
|
||||
@@ -127,5 +127,5 @@ slug: birdnet-go-dev
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
usb: true
|
||||
version: "source-20260714"
|
||||
version: "source-20260716"
|
||||
video: true
|
||||
|
||||
@@ -77,10 +77,27 @@ for entry in "${prs[@]}"; do
|
||||
# Fetch the PR head commit by number; works unauthenticated for public repos.
|
||||
git fetch --no-tags origin "refs/pull/${number}/head"
|
||||
if ! git merge --no-edit --no-ff -m "Merge PR #${number}: ${title}" "${sha}"; then
|
||||
echo "!!! Merge conflict while merging PR #${number} (${title})." >&2
|
||||
echo "!!! Resolve the conflict in the fork or pause this PR, then rebuild." >&2
|
||||
git merge --abort || true
|
||||
exit 1
|
||||
mapfile -t conflicted_files < <(git diff --name-only --diff-filter=U)
|
||||
|
||||
# package-lock.json is generated content and stacked PRs can carry an
|
||||
# older copy even when their source changes merge cleanly. Keep the
|
||||
# lockfile already assembled from upstream and earlier PRs, but only
|
||||
# when it is the sole conflict. Any source conflict remains fatal.
|
||||
if [ "${#conflicted_files[@]}" -eq 1 ] \
|
||||
&& [ "${conflicted_files[0]}" = "frontend/package-lock.json" ]; then
|
||||
log "Resolving generated frontend/package-lock.json conflict using the accumulated tree"
|
||||
git checkout --ours -- frontend/package-lock.json
|
||||
git add frontend/package-lock.json
|
||||
git commit --no-edit
|
||||
else
|
||||
echo "!!! Merge conflict while merging PR #${number} (${title})." >&2
|
||||
if [ "${#conflicted_files[@]}" -gt 0 ]; then
|
||||
printf '!!! Conflicting file: %s\n' "${conflicted_files[@]}" >&2
|
||||
fi
|
||||
echo "!!! Resolve the conflict in the fork or pause this PR, then rebuild." >&2
|
||||
git merge --abort || true
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
## 0.144.3-2 (14-07-2026)
|
||||
|
||||
- Initial ChatGPT Codex add-on.
|
||||
- Added a persistent, administrator-only Home Assistant ingress terminal backed by tmux.
|
||||
- Made `headroom wrap codex` the default launch path.
|
||||
- Configured Docker builds to install the latest stable Codex, Headroom, RTK, ttyd, and Rust toolchain versions without hard-coded tool version pins.
|
||||
- Added RTK native Codex initialization and savings reporting.
|
||||
- Added device-code authentication and direct Codex fallback helpers.
|
||||
- Added persistent configuration, GitHub CLI integration, mount support, and safe defaults.
|
||||
- Made the default workspace follow a custom `data_location`.
|
||||
@@ -1,157 +0,0 @@
|
||||
#============================#
|
||||
# ALEXBELGIUM'S DOCKERFILE #
|
||||
#============================#
|
||||
#=== Home Assistant Addon ===#
|
||||
|
||||
#################
|
||||
# 1 Build Image #
|
||||
#################
|
||||
|
||||
ARG BUILD_FROM
|
||||
ARG BUILD_VERSION
|
||||
|
||||
FROM rust:bookworm AS rtk-builder
|
||||
RUN set -eux; \
|
||||
rtk_version="$(git ls-remote --tags --refs --sort=-v:refname \
|
||||
https://github.com/rtk-ai/rtk.git 'refs/tags/v*' \
|
||||
| awk -F/ '$3 ~ /^v[0-9]+\.[0-9]+\.[0-9]+$/ { print $3; exit }')"; \
|
||||
test -n "$rtk_version"; \
|
||||
git clone --depth 1 --branch "$rtk_version" https://github.com/rtk-ai/rtk.git /src/rtk; \
|
||||
cd /src/rtk; \
|
||||
cargo build --release --locked; \
|
||||
install -D -m 0755 target/release/rtk /out/rtk; \
|
||||
/out/rtk --version
|
||||
|
||||
FROM ${BUILD_FROM}
|
||||
|
||||
##################
|
||||
# 2 Modify Image #
|
||||
##################
|
||||
|
||||
ENV S6_CMD_WAIT_FOR_SERVICES=1 \
|
||||
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
|
||||
S6_SERVICES_GRACETIME=0 \
|
||||
HEADROOM_CONTEXT_TOOL=rtk
|
||||
|
||||
USER root
|
||||
VOLUME [ "/sys/fs/cgroup" ]
|
||||
|
||||
ARG TEMPLATE_BASE_URL="https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates"
|
||||
|
||||
##################
|
||||
# 3 Install apps #
|
||||
##################
|
||||
|
||||
COPY rootfs/ /
|
||||
RUN find /etc/cont-init.d /etc/s6-overlay /usr/local/bin \
|
||||
-type f \( -name "*.sh" -o -name "run" -o -name "finish" -o -path "/usr/local/bin/*" \) \
|
||||
-print -exec chmod +x {} \;
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
gh \
|
||||
jq \
|
||||
less \
|
||||
nano \
|
||||
openssh-client \
|
||||
python3-pip \
|
||||
ripgrep \
|
||||
tmux && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install the latest stable official Codex static binary for the target architecture.
|
||||
RUN set -eux; \
|
||||
case "$(dpkg --print-architecture)" in \
|
||||
amd64) codex_arch="x86_64" ;; \
|
||||
arm64) codex_arch="aarch64" ;; \
|
||||
*) echo "Unsupported architecture: $(dpkg --print-architecture)" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
archive="/tmp/codex.tar.gz"; \
|
||||
curl -fsSL --retry 3 --retry-delay 2 \
|
||||
-o "$archive" \
|
||||
"https://github.com/openai/codex/releases/latest/download/codex-${codex_arch}-unknown-linux-musl.tar.gz"; \
|
||||
tar -xzf "$archive" -C /tmp; \
|
||||
install -m 0755 "/tmp/codex-${codex_arch}-unknown-linux-musl" /usr/local/bin/codex; \
|
||||
rm -f "$archive" "/tmp/codex-${codex_arch}-unknown-linux-musl"; \
|
||||
codex --version
|
||||
|
||||
# Install the latest stable ttyd binary for the Home Assistant ingress terminal.
|
||||
RUN set -eux; \
|
||||
case "$(dpkg --print-architecture)" in \
|
||||
amd64) ttyd_arch="x86_64" ;; \
|
||||
arm64) ttyd_arch="aarch64" ;; \
|
||||
*) echo "Unsupported architecture: $(dpkg --print-architecture)" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
curl -fsSL --retry 3 --retry-delay 2 \
|
||||
-o /usr/local/bin/ttyd \
|
||||
"https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.${ttyd_arch}"; \
|
||||
chmod 0755 /usr/local/bin/ttyd; \
|
||||
ttyd --version
|
||||
|
||||
COPY --from=rtk-builder /out/rtk /usr/local/bin/rtk
|
||||
RUN rtk --version && \
|
||||
pip3 install --upgrade --break-system-packages --no-cache-dir "headroom-ai[proxy,code,mcp]" && \
|
||||
headroom --version
|
||||
|
||||
ARG MODULES="00-banner.sh 00-global_var.sh 01-custom_script.sh 00-local_mounts.sh 00-smb_mounts.sh 90-dns_set.sh"
|
||||
RUN curl -fsSL --retry 3 --retry-delay 2 \
|
||||
-o /ha_automodules.sh "${TEMPLATE_BASE_URL}/ha_automodules.sh" && \
|
||||
chmod 744 /ha_automodules.sh && \
|
||||
/ha_automodules.sh "$MODULES" && \
|
||||
rm /ha_automodules.sh
|
||||
|
||||
################
|
||||
# 4 Entrypoint #
|
||||
################
|
||||
|
||||
RUN curl -fsSL --retry 3 --retry-delay 2 \
|
||||
-o /ha_entrypoint.sh "${TEMPLATE_BASE_URL}/ha_entrypoint.sh" && \
|
||||
curl -fsSL --retry 3 --retry-delay 2 \
|
||||
-o /usr/local/lib/bashio-standalone.sh "${TEMPLATE_BASE_URL}/bashio-standalone.sh" && \
|
||||
chmod 0777 /ha_entrypoint.sh && \
|
||||
chmod 0755 /usr/local/lib/bashio-standalone.sh
|
||||
|
||||
ENTRYPOINT [ "/usr/bin/env" ]
|
||||
CMD [ "/ha_entrypoint.sh" ]
|
||||
|
||||
############
|
||||
# 5 Labels #
|
||||
############
|
||||
|
||||
ARG BUILD_ARCH
|
||||
ARG BUILD_DATE
|
||||
ARG BUILD_DESCRIPTION
|
||||
ARG BUILD_NAME
|
||||
ARG BUILD_REF
|
||||
ARG BUILD_REPOSITORY
|
||||
ARG BUILD_VERSION
|
||||
ENV BUILD_VERSION="${BUILD_VERSION}"
|
||||
LABEL \
|
||||
io.hass.name="${BUILD_NAME}" \
|
||||
io.hass.description="${BUILD_DESCRIPTION}" \
|
||||
io.hass.arch="${BUILD_ARCH}" \
|
||||
io.hass.type="addon" \
|
||||
io.hass.version=${BUILD_VERSION} \
|
||||
maintainer="alexbelgium (https://github.com/alexbelgium)" \
|
||||
org.opencontainers.image.title="${BUILD_NAME}" \
|
||||
org.opencontainers.image.description="${BUILD_DESCRIPTION}" \
|
||||
org.opencontainers.image.vendor="Home Assistant Add-ons" \
|
||||
org.opencontainers.image.authors="alexbelgium (https://github.com/alexbelgium)" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.url="https://github.com/alexbelgium" \
|
||||
org.opencontainers.image.source="https://github.com/${BUILD_REPOSITORY}" \
|
||||
org.opencontainers.image.documentation="https://github.com/${BUILD_REPOSITORY}/blob/master/chatgpt_codex/README.md" \
|
||||
org.opencontainers.image.created=${BUILD_DATE} \
|
||||
org.opencontainers.image.revision=${BUILD_REF} \
|
||||
org.opencontainers.image.version=${BUILD_VERSION}
|
||||
|
||||
#################
|
||||
# 6 Healthcheck #
|
||||
#################
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s \
|
||||
CMD curl -fsS http://127.0.0.1:7681/ > /dev/null || exit 1
|
||||
@@ -1,112 +0,0 @@
|
||||
# Home Assistant add-on: ChatGPT Codex
|
||||
|
||||
![Supports aarch64 Architecture][aarch64-shield]
|
||||
![Supports amd64 Architecture][amd64-shield]
|
||||
![Project Maintenance][maintenance-shield]
|
||||
|
||||
Run the official OpenAI Codex CLI in a persistent Home Assistant ingress terminal. The optimized path uses `headroom wrap codex`, with RTK handling command-output compression before results reach Codex.
|
||||
|
||||
> The repository already contains an unrelated add-on named **Codex** for comic archives. This coding-agent add-on therefore uses the slug `chatgpt_codex`.
|
||||
|
||||
## Features
|
||||
|
||||
- Latest stable Codex, Headroom, RTK, ttyd, and Rust toolchain versions are resolved during every Docker build; tool versions are not pinned in the Dockerfile.
|
||||
- Official Codex CLI static binary for `amd64` and `aarch64`.
|
||||
- Home Assistant authenticated, administrator-only ingress; no unauthenticated terminal port is exposed.
|
||||
- Persistent `$HOME`, Codex authentication, settings, sessions, Headroom state, and RTK statistics.
|
||||
- Persistent `tmux` session that survives browser disconnects.
|
||||
- `headroom wrap codex` as the default launch path.
|
||||
- Baked-in RTK with native Codex initialization.
|
||||
- Optional Headroom output shaping and code-aware compression.
|
||||
- Direct Codex fallback for troubleshooting.
|
||||
- Device-code login helper designed for a remote or headless container.
|
||||
- Baked-in Git, GitHub CLI, ripgrep, jq, SSH client, and common terminal tools.
|
||||
- Optional GitHub CLI authentication and Git author configuration.
|
||||
- Optional extra apt and pip packages.
|
||||
- Local and SMB mount support through the repository standard modules.
|
||||
|
||||
## Installation and first login
|
||||
|
||||
1. Install **ChatGPT Codex** from this add-on repository.
|
||||
2. Keep the default `data_location` and `workspace`, or select writable mounted paths.
|
||||
3. Start the add-on and open its web UI.
|
||||
4. Codex starts automatically through Headroom.
|
||||
5. When prompted to authenticate, follow the device-code instructions. You can also exit Codex and run:
|
||||
|
||||
```shell
|
||||
codex-login
|
||||
```
|
||||
|
||||
Codex supports ChatGPT sign-in and API-key authentication. The device-code flow is the recommended option for this headless add-on.
|
||||
|
||||
## Launch commands
|
||||
|
||||
Optimized default:
|
||||
|
||||
```shell
|
||||
codex-headroom
|
||||
```
|
||||
|
||||
This runs:
|
||||
|
||||
```shell
|
||||
headroom wrap codex
|
||||
```
|
||||
|
||||
Headroom starts its local proxy, configures Codex routing and MCP support, and uses RTK as the CLI context tool.
|
||||
|
||||
Direct troubleshooting path:
|
||||
|
||||
```shell
|
||||
codex-direct
|
||||
```
|
||||
|
||||
Check optimization status and measured savings:
|
||||
|
||||
```shell
|
||||
headroom doctor
|
||||
headroom perf
|
||||
rtk gain
|
||||
```
|
||||
|
||||
## Persistence
|
||||
|
||||
The terminal attaches every browser connection to the same `tmux` session. Closing the browser detaches the client but does not stop Codex or commands running in the session.
|
||||
|
||||
Persistent data is stored below `data_location`:
|
||||
|
||||
- Codex state: `~/.codex`
|
||||
- Headroom state and metrics: `~/.headroom`
|
||||
- RTK state: its normal paths below the persistent home
|
||||
- Default workspace: `~/workspace`
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `data_location` | `/data/data` | Persistent home. Must be below `/data`, `/share`, `/media`, `/config`, or `/mnt`. |
|
||||
| `workspace` | `<data_location>/workspace` | Initial project directory. Leave empty to follow `data_location`. |
|
||||
| `PUID` / `PGID` | `0` / `0` | Runtime user and group used by the LinuxServer `abc` account. |
|
||||
| `TZ` | | Optional timezone, for example `Europe/Brussels`. |
|
||||
| `auto_start_codex` | `true` | Start Codex automatically when the tmux session is first created. |
|
||||
| `use_headroom` | `true` | Use `headroom wrap codex`; disabling this starts Codex directly. |
|
||||
| `headroom_output_shaper` | `true` | Enable Headroom output-token shaping. |
|
||||
| `headroom_code_aware` | `true` | Enable Headroom AST-aware code compression. |
|
||||
| `github_token` | | Authenticate GitHub CLI and Git operations. |
|
||||
| `github_username` / `github_email` | | Configure the global Git author. |
|
||||
| `additional_apps` | | Comma-separated Debian packages installed at startup. |
|
||||
| `additional_pip` | | Comma-separated Python packages installed at startup. |
|
||||
| `localdisks` / `networkdisks` | | Optional local-disk and SMB mounts supported by the repository modules. |
|
||||
| `env_vars` | `[]` | Additional environment variables exported in the container. |
|
||||
|
||||
Configuration changes affecting the launch command apply to a newly created tmux session. To recreate it, exit Codex and run `tmux kill-session -t codex`, then reopen the add-on web UI.
|
||||
|
||||
## Security
|
||||
|
||||
The add-on deliberately does not enable Codex approval or sandbox bypass flags. Codex can execute commands and edit files available inside the configured workspace, so only mount locations you intend it to access.
|
||||
|
||||
The terminal is exposed only through Home Assistant administrator-only ingress. Do not add an unauthenticated direct port mapping. Treat `github_token`, Codex authentication data, and the persistent home as secrets and include them only in trusted backups.
|
||||
|
||||
[aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg
|
||||
[amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg
|
||||
[maintenance-shield]: https://img.shields.io/maintenance/yes/2026.svg
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"build_from": {
|
||||
"aarch64": "ghcr.io/linuxserver/baseimage-debian:arm64v8-bookworm",
|
||||
"amd64": "ghcr.io/linuxserver/baseimage-debian:amd64-bookworm"
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
arch:
|
||||
- aarch64
|
||||
- amd64
|
||||
description: "Persistent OpenAI Codex web terminal optimized with Headroom and RTK"
|
||||
devices:
|
||||
- /dev/fuse
|
||||
environment:
|
||||
HOME: /data/data
|
||||
PGID: "0"
|
||||
PUID: "0"
|
||||
TERM: xterm-256color
|
||||
image: ghcr.io/alexbelgium/chatgpt_codex-{arch}
|
||||
ingress: true
|
||||
ingress_port: 7681
|
||||
ingress_stream: true
|
||||
init: false
|
||||
map:
|
||||
- addon_config:rw
|
||||
- share:rw
|
||||
- media:rw
|
||||
- ssl
|
||||
name: ChatGPT Codex
|
||||
options:
|
||||
env_vars: []
|
||||
DNS_server: 8.8.8.8
|
||||
data_location: /data/data
|
||||
workspace: ""
|
||||
PUID: 0
|
||||
PGID: 0
|
||||
auto_start_codex: true
|
||||
use_headroom: true
|
||||
headroom_output_shaper: true
|
||||
headroom_code_aware: true
|
||||
github_token: ""
|
||||
github_username: ""
|
||||
github_email: ""
|
||||
additional_apps: ""
|
||||
additional_pip: ""
|
||||
panel_icon: mdi:code-braces-box
|
||||
privileged:
|
||||
- SYS_ADMIN
|
||||
- DAC_READ_SEARCH
|
||||
schema:
|
||||
env_vars:
|
||||
- name: match(^[A-Za-z0-9_]+$)
|
||||
value: str?
|
||||
DNS_server: str?
|
||||
data_location: str?
|
||||
workspace: str?
|
||||
PUID: int
|
||||
PGID: int
|
||||
TZ: match([A-Z][a-z]*./[A-Z][a-z]*.)?
|
||||
auto_start_codex: bool
|
||||
use_headroom: bool
|
||||
headroom_output_shaper: bool
|
||||
headroom_code_aware: bool
|
||||
github_token: password?
|
||||
github_username: str?
|
||||
github_email: str?
|
||||
additional_apps: str?
|
||||
additional_pip: str?
|
||||
localdisks: str?
|
||||
networkdisks: str?
|
||||
slug: chatgpt_codex
|
||||
tmpfs: true
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "0.144.3-3"
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
PUID="$(bashio::config 'PUID')"
|
||||
PGID="$(bashio::config 'PGID')"
|
||||
LOCATION="$(bashio::config 'data_location')"
|
||||
|
||||
if [ -z "$LOCATION" ] || [ "$LOCATION" = "null" ]; then
|
||||
LOCATION="/data/data"
|
||||
fi
|
||||
|
||||
case "$LOCATION" in
|
||||
/data/* | /share/* | /media/* | /config/* | /mnt/*)
|
||||
;;
|
||||
*)
|
||||
bashio::log.fatal "data_location must be below /data, /share, /media, /config, or /mnt"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -L "$LOCATION" ]; then
|
||||
bashio::log.fatal "data_location must not be a symbolic link"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bashio::log.info "Using persistent home: $LOCATION"
|
||||
install -d -m 0750 -o "$PUID" -g "$PGID" "$LOCATION"
|
||||
install -d -m 0750 -o "$PUID" -g "$PGID" "$LOCATION/.codex" "$LOCATION/.headroom"
|
||||
install -d -m 0755 /tmp/cache /run/s6/container_environment
|
||||
|
||||
sed -i "s|^\(abc:[^:]*:[^:]*:[^:]*:[^:]*:\)[^:]*|\1$LOCATION|" /etc/passwd
|
||||
|
||||
for variable in HOME CODEX_HOME HEADROOM_WORKSPACE_DIR XDG_CACHE_HOME; do
|
||||
case "$variable" in
|
||||
HOME) value="$LOCATION" ;;
|
||||
CODEX_HOME) value="$LOCATION/.codex" ;;
|
||||
HEADROOM_WORKSPACE_DIR) value="$LOCATION/.headroom" ;;
|
||||
XDG_CACHE_HOME) value="/tmp/cache" ;;
|
||||
esac
|
||||
printf '%s' "$value" > "/run/s6/container_environment/$variable"
|
||||
done
|
||||
|
||||
chown -R "$PUID:$PGID" "$LOCATION/.codex" "$LOCATION/.headroom"
|
||||
chown "$PUID:$PGID" "$LOCATION"
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
if bashio::config.has_value 'additional_apps'; then
|
||||
packages="$(bashio::config 'additional_apps')"
|
||||
apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10
|
||||
for package in ${packages//,/ }; do
|
||||
bashio::log.info "Installing apt package: $package"
|
||||
apt-get install -y --no-install-recommends "$package"
|
||||
done
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
fi
|
||||
|
||||
if bashio::config.has_value 'additional_pip'; then
|
||||
packages="$(bashio::config 'additional_pip')"
|
||||
for package in ${packages//,/ }; do
|
||||
bashio::log.info "Installing pip package: $package"
|
||||
pip3 install --break-system-packages "$package"
|
||||
done
|
||||
fi
|
||||
|
||||
if bashio::config.has_value 'TZ'; then
|
||||
timezone="$(bashio::config 'TZ')"
|
||||
if [ ! -e "/usr/share/zoneinfo/$timezone" ]; then
|
||||
bashio::log.fatal "Invalid timezone: $timezone"
|
||||
exit 1
|
||||
fi
|
||||
ln -snf "/usr/share/zoneinfo/$timezone" /etc/localtime
|
||||
printf '%s\n' "$timezone" > /etc/timezone
|
||||
fi
|
||||
@@ -1,55 +0,0 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
PUID="$(bashio::config 'PUID')"
|
||||
PGID="$(bashio::config 'PGID')"
|
||||
|
||||
if ! command -v codex > /dev/null 2>&1; then
|
||||
bashio::log.fatal "Codex CLI is not available"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v headroom > /dev/null 2>&1; then
|
||||
bashio::log.fatal "Headroom is not available"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v rtk > /dev/null 2>&1; then
|
||||
bashio::log.fatal "RTK is not available"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bashio::log.info "Codex: $(codex --version 2>&1 | head -n 1)"
|
||||
bashio::log.info "Headroom: $(headroom --version 2>&1 | head -n 1)"
|
||||
bashio::log.info "RTK: $(rtk --version 2>&1 | head -n 1)"
|
||||
|
||||
# Configure RTK's native Codex integration ahead of the first wrapped session.
|
||||
if ! s6-setuidgid abc env \
|
||||
HOME="$HOME" \
|
||||
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" \
|
||||
PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin" \
|
||||
RTK_NONINTERACTIVE=1 \
|
||||
rtk init -g --codex; then
|
||||
bashio::log.warning "RTK Codex initialization failed; Headroom will retry when wrapping Codex"
|
||||
fi
|
||||
|
||||
for key in CODEX_AUTO_START CODEX_USE_HEADROOM HEADROOM_OUTPUT_SHAPER HEADROOM_CODE_AWARE_ENABLED; do
|
||||
case "$key" in
|
||||
CODEX_AUTO_START)
|
||||
bashio::config.true 'auto_start_codex' && value="1" || value="0"
|
||||
;;
|
||||
CODEX_USE_HEADROOM)
|
||||
bashio::config.true 'use_headroom' && value="1" || value="0"
|
||||
;;
|
||||
HEADROOM_OUTPUT_SHAPER)
|
||||
bashio::config.true 'headroom_output_shaper' && value="1" || value="0"
|
||||
;;
|
||||
HEADROOM_CODE_AWARE_ENABLED)
|
||||
bashio::config.true 'headroom_code_aware' && value="1" || value="0"
|
||||
;;
|
||||
esac
|
||||
printf '%s' "$value" > "/run/s6/container_environment/$key"
|
||||
done
|
||||
printf '%s' 'rtk' > /run/s6/container_environment/HEADROOM_CONTEXT_TOOL
|
||||
|
||||
chown -R "$PUID:$PGID" "$HOME/.codex" "$HOME/.headroom"
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
if bashio::config.has_value 'github_username'; then
|
||||
s6-setuidgid abc git config --global user.name "$(bashio::config 'github_username')"
|
||||
fi
|
||||
|
||||
if bashio::config.has_value 'github_email'; then
|
||||
s6-setuidgid abc git config --global user.email "$(bashio::config 'github_email')"
|
||||
fi
|
||||
|
||||
if bashio::config.has_value 'github_token'; then
|
||||
token="$(bashio::config 'github_token')"
|
||||
if s6-setuidgid abc env -u GH_TOKEN -u GITHUB_TOKEN gh auth status --hostname github.com > /dev/null 2>&1; then
|
||||
bashio::log.info "GitHub CLI is already authenticated"
|
||||
else
|
||||
bashio::log.info "Authenticating GitHub CLI"
|
||||
printf '%s\n' "$token" | s6-setuidgid abc env -u GH_TOKEN -u GITHUB_TOKEN \
|
||||
gh auth login --hostname github.com --with-token || \
|
||||
bashio::log.warning "GitHub CLI authentication failed"
|
||||
fi
|
||||
s6-setuidgid abc env -u GH_TOKEN -u GITHUB_TOKEN \
|
||||
gh auth setup-git --hostname github.com || \
|
||||
bashio::log.warning "GitHub CLI git credential setup failed"
|
||||
else
|
||||
bashio::log.info "Set github_token to authenticate gh and Git operations"
|
||||
fi
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
port=7681
|
||||
workspace="$(bashio::config 'workspace')"
|
||||
|
||||
if [ -z "$workspace" ] || [ "$workspace" = "null" ]; then
|
||||
workspace="$HOME/workspace"
|
||||
fi
|
||||
|
||||
if [[ "$workspace" != /* ]]; then
|
||||
bashio::log.fatal "workspace must be an absolute path"
|
||||
exec sleep infinity
|
||||
fi
|
||||
if [ -L "$workspace" ]; then
|
||||
bashio::log.fatal "workspace must not be a symbolic link"
|
||||
exec sleep infinity
|
||||
fi
|
||||
|
||||
workspace="$(realpath -m -- "$workspace")"
|
||||
case "$workspace" in
|
||||
"$HOME" | "$HOME"/* | /share/* | /media/* | /mnt/* | /data/* | /config/*)
|
||||
;;
|
||||
*)
|
||||
bashio::log.fatal "workspace must be below the persistent home, /share, /media, /mnt, /data, or /config"
|
||||
exec sleep infinity
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ ! -e "$workspace" ]; then
|
||||
install -d -m 0750 -o abc -g abc "$workspace"
|
||||
elif [ ! -d "$workspace" ]; then
|
||||
bashio::log.fatal "workspace is not a directory: $workspace"
|
||||
exec sleep infinity
|
||||
fi
|
||||
|
||||
if ! s6-setuidgid abc test -r "$workspace" || \
|
||||
! s6-setuidgid abc test -w "$workspace" || \
|
||||
! s6-setuidgid abc test -x "$workspace"; then
|
||||
bashio::log.fatal "workspace must be readable, writable, and searchable by user abc: $workspace"
|
||||
exec sleep infinity
|
||||
fi
|
||||
|
||||
export CODEX_TERMINAL_WORKSPACE="$workspace"
|
||||
bashio::log.info "Starting persistent Codex terminal on Home Assistant ingress port $port"
|
||||
exec s6-setuidgid abc ttyd \
|
||||
-p "$port" \
|
||||
-W \
|
||||
-O \
|
||||
-t disableLeaveAlert=true \
|
||||
-t fontSize=14 \
|
||||
/usr/local/bin/codex-terminal-shell
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
|
||||
exec codex "$@"
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
|
||||
export HEADROOM_CONTEXT_TOOL="rtk"
|
||||
|
||||
if ! command -v headroom > /dev/null 2>&1; then
|
||||
echo "Headroom is unavailable; launching Codex directly." >&2
|
||||
exec codex "$@"
|
||||
fi
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
exec headroom wrap codex
|
||||
fi
|
||||
exec headroom wrap codex -- "$@"
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
|
||||
exec codex login --device-auth "$@"
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export SHELL="/bin/bash"
|
||||
export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
|
||||
|
||||
workspace="${CODEX_TERMINAL_WORKSPACE:-${HOME}/workspace}"
|
||||
session_name="${CODEX_TMUX_SESSION:-codex}"
|
||||
|
||||
if [ ! -d "$workspace" ]; then
|
||||
echo "Codex workspace does not exist: $workspace" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
new_session=0
|
||||
if ! tmux has-session -t "$session_name" 2> /dev/null; then
|
||||
tmux new-session -d -s "$session_name" -c "$workspace" /bin/bash -l
|
||||
new_session=1
|
||||
fi
|
||||
|
||||
if [ "$new_session" -eq 1 ]; then
|
||||
tmux send-keys -t "$session_name" \
|
||||
"printf '\\nChatGPT Codex add-on\\n login: codex-login\\n optimized: codex-headroom\\n direct: codex-direct\\n savings: rtk gain && headroom perf\\n\\n'" C-m
|
||||
if [ "${CODEX_AUTO_START:-1}" = "1" ]; then
|
||||
if [ "${CODEX_USE_HEADROOM:-1}" = "1" ]; then
|
||||
tmux send-keys -t "$session_name" "codex-headroom" C-m
|
||||
else
|
||||
tmux send-keys -t "$session_name" "codex-direct" C-m
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
exec tmux attach-session -t "$session_name"
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"github_beta": false,
|
||||
"github_fulltag": false,
|
||||
"github_havingasset": true,
|
||||
"github_tagfilter": "rust-v",
|
||||
"last_update": "2026-07-14",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "chatgpt_codex",
|
||||
"source": "github",
|
||||
"upstream_repo": "openai/codex",
|
||||
"upstream_version": "0.144.3"
|
||||
}
|
||||
@@ -1,4 +1,65 @@
|
||||
## 1.17 (14-07-2026)
|
||||
## 1.31 (16-07-2026)
|
||||
|
||||
- Fix Home Assistant ingress remaining on `Waiting for stream`: current Selkies WebSocket mode connects through `/api/websockets`, while the add-on inherited an older nginx template that proxied only `/websocket`. Replace the template rewrite with an explicit ingress server that proxies `/api/` to Selkies on port 8082, retains `/websocket` compatibility, and declares `ingress_port: 3001` so nginx always receives a valid listen port.
|
||||
|
||||
## 1.30 (16-07-2026)
|
||||
|
||||
- Compress large tool outputs automatically in every Claude Code session with a managed `PostToolUse` hook (new `headroom_auto_compress` option, enabled by default). Desktop-spawned sessions (cowork/dispatch) pin `ANTHROPIC_BASE_URL` to the production endpoint (headroom #869), so the transparent proxy never sees their traffic and compression there depended entirely on the model remembering to call the `headroom` MCP tools per the CLAUDE.md guidance — in practice most large outputs went uncompressed. The new `/usr/local/bin/headroom-posttooluse-compress.py` hook fires on `Bash`/`Grep`/`Glob`/`WebFetch` results over ~4000 characters, compresses them with Headroom's rule-based pipeline (SmartCrusher and friends; the Kompress ML path is disabled because its background model load can never complete inside a short-lived hook process), and swaps the result in via `hookSpecificOutput.updatedToolOutput` with a retrieval marker appended. Originals are stored in the shared CCR SQLite store (`~/.headroom/ccr_store.db` — the same one the headroom MCP server reads), so `mcp__headroom__headroom_retrieve` always recovers the full output; savings are recorded to the durable ledger (client `posttooluse-hook`) and show up in the existing gains report. The hook fails open (any error leaves the tool output untouched), never touches `stderr` fields so error text reaches the model verbatim, skips anything below a 50-token savings floor, and is registered idempotently in `~/.claude/settings.json` only after a `--self-test` confirms the interpreter can import headroom; disabling the option (or Headroom) removes the managed entry without touching user-defined hooks. Measured on a representative Home Assistant `states` dump: 10781 -> 2964 tokens (73% saved) at ~1.7 s hook overhead, with sub-100 ms pass-through for small outputs.
|
||||
|
||||
## 1.29 (16-07-2026)
|
||||
|
||||
- Point the Headroom MCP server at the persistent Kompress model cache. 1.27 set `HF_HOME` on the `svc-headroom` proxy longrun only, but the MCP server is a separate process spawned by Claude Desktop / Claude Code from the registered `mcpServers` entry, so it never inherited that export and kept resolving the HuggingFace cache to `~/.cache` — symlinked to tmpfs here and wiped on every restart. Its Kompress ML path therefore never found the model, re-downloaded ~270 MB into tmpfs on each boot, and lost it again on the next one; `headroom_compress` fell back to `router:noop` (unchanged output) on prose and other unstructured content. The managed `headroom` entry in both `claude_desktop_config.json` and `~/.claude.json` now carries `env.HF_HOME` pointing at the same `~/.headroom/hf` cache the proxy warms. Rule-based compression (SmartCrusher, structured tool output) was unaffected and worked throughout.
|
||||
- Fix `~/.gitconfig` being written as `root` and left unreadable by the `abc` runtime user, which broke git for the user that actually runs it: every commit failed with `Author identity unknown` and the `gh` credential helper was invisible to authenticated pushes. `git config --global` ran as root during init and rewrites the file on every start, so `20-folders.sh`'s earlier recursive chown never stuck to it (`.config/gh` survived abc-owned only because the "already authenticated" branch skips rewriting it). The git/gh setup now runs as `abc` via `s6-setuidgid`, matching `81-tokensave_repositories.sh`, and reclaims any root-owned copies left by an earlier version before writing.
|
||||
- Fix `~/.bashrc` accumulating stale `HOME`/`FM_HOME` exports when `data_location` changes. The idempotency guard only tested for the *current* `$LOCATION`, so changing the option and later changing it back appended a second block while leaving the first, and the last one written won for every interactive shell — leaving `$HOME` pointing at a directory the add-on no longer manages. Any tool that resolves config through `$HOME` then read the wrong path (`headroom doctor` reported `claude: not routed (no ~/.claude/settings.json)` against a correctly routed install, and bare `headroom` invocations created a stray `.headroom` tree under the old location). The block is now marker-delimited and rewritten from scratch on every boot, so it is idempotent across any number of `data_location` changes.
|
||||
|
||||
## 1.27 (15-07-2026)
|
||||
|
||||
- Route Claude Desktop cowork/local-agent-mode sessions through the Headroom proxy. Desktop spawns its bundled Claude Code binary at an absolute path (bypassing the add-on's PATH wrapper) with `ANTHROPIC_BASE_URL` pinned to the production endpoint, so those sessions never produced proxy savings. The add-on now manages `env.ANTHROPIC_BASE_URL` in `~/.claude/settings.json` — settings `env` entries replace inherited environment values at CLI startup — gated on `headroom_wrap_claude_code` and never overwriting a user-customized endpoint.
|
||||
- Fix Headroom's Kompress compression engine never activating, which made even proxied traffic record zero token savings (e.g. 175 requests, 0 saved). The proxy's startup preload is deliberately cache-only, but the HuggingFace model cache defaulted to `~/.cache` — tmpfs in this add-on, wiped every restart — so the ONNX model (plus the separately fetched `answerdotai/ModernBERT-base` tokenizer) was never cached and the engine idled in "deferred" mode forever, misleadingly logged as `Kompress: not installed`. `svc-headroom` now points `HF_HOME` at persistent storage (`~/.headroom/hf`, ~270 MB); the proxy's own request path already downloads a missing model in the background on first use and passes requests through uncompressed until it lands, so no blocking startup pre-warm is needed — the port binds immediately either way, and Kompress activates within the first couple of requests on the first boot, then loads instantly on every boot after. The already-installed `proxy` extra's ONNX runtime is sufficient — the multi-gigabyte PyTorch `ml` extra is deliberately not installed.
|
||||
|
||||
## 1.26 (15-07-2026)
|
||||
|
||||
- Fix startup permission failures that prevented Claude Desktop from starting: storage was chowned to a hardcoded `1000:1000`, but the shared `abc` desktop user was never mapped to that UID. During init `abc` was still the image default (`911`), so TokenSave (`.claude.json.new`), RTK (`RTK.md`), nginx, PulseAudio, the Mesa shader cache, and Claude Desktop itself all hit `Permission denied`; the base image's `init-adduser` then remapped `abc` to root mid-startup (PUID/PGID were read from add-on options where they did not exist, falling back to `0`), which also made Claude Code reject `permission_mode: bypass`.
|
||||
- Add `PUID`/`PGID` add-on options (default `1000:1000`) and remap `abc` to that identity at the very start of folder setup, before any ownership is applied and before any service resolves the user. The base image's `init-adduser` is pinned to the same effective identity so it can no longer remap `abc` mid-startup.
|
||||
- In `permission_mode: bypass`, a configured `PUID: 0` automatically falls back to UID `1000` (Claude Code refuses bypass permissions as root), retaining the configured group.
|
||||
- Fix `bashio::config.array: command not found` in the TokenSave repository setup, tools configuration, and `claude-tools-doctor.sh`: the function only exists in the repo's standalone bashio, not in the real bashio shipped in the image. Use `bashio::config`, which prints list entries one per line.
|
||||
- Return managed Claude configuration files to the effective `abc` identity instead of the raw configured `PUID`/`PGID` (which previously fell back to `0` and left the files root-owned).
|
||||
- Pre-create `/tmp/.X11-unix` with the standard sticky mode so Xorg, which runs as the non-root `abc` user on a tmpfs `/tmp`, no longer fails to create its socket directory (`_XSERVTransmkdir: euid != 0`).
|
||||
|
||||
## 1.25 (15-07-2026)
|
||||
- Minor bugs fixed
|
||||
## 1.24 (15-07-2026)
|
||||
|
||||
- Fix the `/usr/local/bin/claude` wrapper never routing terminal Claude Code sessions through the Headroom proxy: it hardcoded `HEADROOM_BIN="/usr/local/bin/headroom"` while the binary is installed at `/usr/bin/headroom`, so the executable check always failed and the wrapper fell back to launching Claude Code directly. Resolve the binary with `command -v headroom` instead.
|
||||
- Harden startup TokenSave indexing so an interrupted `init`/`sync` or a hard add-on stop can no longer leave a corrupt semantic graph that fails every subsequent boot. Each configured repository is now prepared under a startup-scoped `flock` (serialised against overlapping restarts and mid-boot git sync hooks); an existing index is refreshed with a retried incremental `sync` (transient `SQLITE_BUSY` no longer looks like corruption); and only a genuinely unreadable index — or a half-written one flagged by an `init` sentinel — is quarantined to `.tokensave/corrupt-<timestamp>/` and rebuilt from scratch, so the graph self-heals instead of propagating corruption.
|
||||
|
||||
## 1.23 (15-07-2026)
|
||||
|
||||
- Add a `ha-cli` helper that lets Claude configure Home Assistant (automations, scripts, scenes, helpers, dashboards, area/label/floor/entity registries, and service calls) through the Home Assistant Core API instead of a filesystem mount. It authenticates automatically with the add-on's `SUPERVISOR_TOKEN` via the Supervisor Core-API proxy (no token setup), and deliberately cannot reach `configuration.yaml`/`secrets.yaml` or other add-ons' credentials. Toggle with the new `enable_ha_api_helper` option (default on), which also controls a managed guidance block appended to `~/.claude/CLAUDE.md`.
|
||||
|
||||
## 1.21 (15-07-2026)
|
||||
|
||||
- Fix Claude Code bypass permissions being rejected when the add-on uses its default root `PUID`.
|
||||
- In `permission_mode: bypass`, remap the shared `abc` Desktop runtime to an unused non-root UID before storage ownership and Selkies startup, while retaining its configured primary group for mounted-path access.
|
||||
- Make folder setup and final Claude configuration ownership follow the effective `abc` identity instead of the configured root UID.
|
||||
- Drop root console invocations of the add-on's `/usr/local/bin/claude` wrapper to the non-root `abc` runtime before passing `--dangerously-skip-permissions`.
|
||||
- Extend `claude-tools-doctor.sh` with configured/effective UID and GID checks for bypass mode.
|
||||
|
||||
## 1.20 (15-07-2026)
|
||||
|
||||
- Complete the TokenSave Claude Code integration at startup: install its MCP server, permissions, PreToolUse/UserPromptSubmit/Stop hooks, global guidance, and Git synchronization hooks instead of registering only `tokensave serve`.
|
||||
- Add `tokensave_project_paths` for explicit per-repository initialization and incremental synchronization; no repositories are scanned or indexed unless listed.
|
||||
- Route PATH-based Claude Code launches through the already-supervised Headroom proxy by default with a recursion-safe `/usr/local/bin/claude` wrapper; fall back to the official binary when the proxy is unavailable.
|
||||
- Pass the local proxy URL explicitly to the Headroom MCP server, while retaining MCP-only integration for the Desktop Electron application.
|
||||
- Keep the unauthenticated Headroom dashboard container-local by default; add `expose_headroom_dashboard` and leave port `8787/tcp` unmapped until explicitly enabled.
|
||||
- Fix the hourly gains report so Headroom no longer suppresses RTK output, add TokenSave gains, and gate each tool on its actual add-on option.
|
||||
- Add `claude-tools-doctor.sh` to inspect binaries, redacted MCP registrations, hooks, proxy health, routing, project indexes, and gains.
|
||||
- Install local validation tools (`jq`, `shellcheck`, `yamllint`, current `hadolint`, and current `actionlint`) to reduce avoidable CI round-trips.
|
||||
- Disable the unpinned third-party Caveman startup installer by default; it remains opt-in.
|
||||
|
||||
## 1.19 (14-07-2026)
|
||||
- Minor bugs fixed
|
||||
## 1.18 (14-07-2026)
|
||||
|
||||
- **Breaking:** remove the standalone Claude Code web terminal (ttyd/tmux service, port `7681`, and the `enable_terminal`, `terminal_username`, `terminal_password`, `terminal_workspace` options). The add-on is now built purely around Claude Desktop; Claude Code remains installed and powers Desktop cowork/dispatch sessions with the RTK hook, Caveman, and MCP servers intact. If the add-on refuses to start after the update, open its Configuration tab and re-save to drop the removed options.
|
||||
- Remove the `claude-direct` and `claude-headroom` terminal wrapper scripts and the unused `ha_smart_context` and `dangerously_skip_permissions` options.
|
||||
@@ -15,7 +76,7 @@
|
||||
- Minor bugs fixed
|
||||
## 1.15 (13-07-2026)
|
||||
- Minor bugs fixed
|
||||
|
||||
|
||||
## ubunturesolute-version-6dc44b0e (2026-07-13)
|
||||
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
|
||||
## 1.14 (10-07-2026)
|
||||
|
||||
@@ -34,6 +34,7 @@ RUN cargo install tokensave --version "${TOKENSAVE_VERSION}" --locked --root /ou
|
||||
/out/bin/tokensave --version
|
||||
|
||||
FROM ${BUILD_FROM}
|
||||
ARG BUILD_ARCH
|
||||
|
||||
##################
|
||||
# 2 Modify Image #
|
||||
@@ -73,15 +74,17 @@ RUN curl -fsSL --retry 3 --retry-delay 2 \
|
||||
# cannot alter executables elsewhere in the image.
|
||||
COPY rootfs/ /
|
||||
RUN find /etc/cont-init.d /etc/s6-overlay /defaults /usr/local/bin -type f \
|
||||
\( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \;
|
||||
\( -name "*.sh" -o -name "run" -o -name "finish" -o -name "ha-cli" \) -print -exec chmod +x {} \; && \
|
||||
chmod +x /usr/local/bin/claude
|
||||
|
||||
# Uses /bin for compatibility purposes
|
||||
# hadolint ignore=DL4005
|
||||
RUN if [ ! -f /bin/sh ] && [ -f /usr/bin/sh ]; then ln -s /usr/bin/sh /bin/sh; fi && \
|
||||
if [ ! -f /bin/bash ] && [ -f /usr/bin/bash ]; then ln -s /usr/bin/bash /bin/bash; fi
|
||||
|
||||
# Install Claude Desktop, Claude Code, and Python tooling. gnome-keyring provides the
|
||||
# Secret Service backend Electron safeStorage needs to persist sign-in and dispatch grants.
|
||||
# Install Claude Desktop, Claude Code, Python tooling, and lightweight local validators.
|
||||
# gnome-keyring provides the Secret Service backend Electron safeStorage needs to persist
|
||||
# sign-in and dispatch grants.
|
||||
RUN install -d -m 0755 /etc/apt/keyrings && \
|
||||
curl -fsSLo /usr/share/keyrings/claude-desktop-archive-keyring.asc https://downloads.claude.ai/claude-desktop/key.asc && \
|
||||
curl -fsSLo /etc/apt/keyrings/claude-code.asc https://downloads.claude.ai/keys/claude-code.asc && \
|
||||
@@ -92,15 +95,41 @@ RUN install -d -m 0755 /etc/apt/keyrings && \
|
||||
claude-desktop \
|
||||
claude-code \
|
||||
python3-pip \
|
||||
gnome-keyring \
|
||||
libsecret-1-0 \
|
||||
dbus-x11 \
|
||||
git \
|
||||
gh \
|
||||
ripgrep && \
|
||||
ripgrep \
|
||||
jq \
|
||||
shellcheck \
|
||||
yamllint && \
|
||||
test -x /usr/bin/claude && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install the current upstream hadolint and actionlint releases for both supported
|
||||
ARG HADOLINT_VERSION=v2.14.0
|
||||
ARG ACTIONLINT_VERSION=v1.7.12
|
||||
|
||||
RUN set -eux; \
|
||||
case "${BUILD_ARCH}" in \
|
||||
amd64) hadolint_arch="x86_64"; actionlint_arch="amd64" ;; \
|
||||
aarch64) hadolint_arch="arm64"; actionlint_arch="arm64" ;; \
|
||||
*) echo "Unsupported validation-tools architecture: ${BUILD_ARCH}" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
curl -fsSL --retry 3 --retry-delay 2 \
|
||||
-o /usr/local/bin/hadolint \
|
||||
"https://github.com/hadolint/hadolint/releases/download/${HADOLINT_VERSION}/hadolint-linux-${hadolint_arch}"; \
|
||||
chmod 0755 /usr/local/bin/hadolint; \
|
||||
curl -fsSL --retry 3 --retry-delay 2 \
|
||||
-o /tmp/actionlint.tar.gz \
|
||||
"https://github.com/rhysd/actionlint/releases/download/${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION#v}_linux_${actionlint_arch}.tar.gz"; \
|
||||
tar -xzf /tmp/actionlint.tar.gz -C /tmp actionlint; \
|
||||
install -m 0755 /tmp/actionlint /usr/local/bin/actionlint; \
|
||||
rm -f /tmp/actionlint /tmp/actionlint.tar.gz; \
|
||||
hadolint --version; \
|
||||
actionlint -version
|
||||
|
||||
# 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
|
||||
@@ -108,11 +137,14 @@ COPY --from=tokensave-builder /out/bin/tokensave /usr/local/bin/tokensave
|
||||
RUN /usr/local/bin/rtk --version && /usr/local/bin/tokensave --version
|
||||
|
||||
# Install only the Headroom proxy, code-compression, and MCP features used by this add-on,
|
||||
# plus mcp-proxy (stdio->SSE bridge for the Home Assistant MCP server) and uv (fast
|
||||
# installer used for the additional_pip option).
|
||||
# plus mcp-proxy (stdio->HTTP bridge for the Home Assistant MCP server) and uv (fast
|
||||
# installer used for the additional_pip option). The `proxy` extra already ships the ONNX
|
||||
# runtime + transformers needed by the Kompress compressor — the `ml` extra (full PyTorch,
|
||||
# ~5 GB with CUDA wheels) is deliberately NOT installed; svc-headroom pre-warms the ONNX
|
||||
# model into the persistent HF cache instead.
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
pip3 install --break-system-packages "headroom-ai[proxy,code,mcp]" mcp-proxy uv && \
|
||||
pip3 install --break-system-packages "headroom-ai[proxy,code,mcp]" mcp-proxy uv websockets && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /root/.cache
|
||||
|
||||
@@ -160,7 +192,6 @@ CMD [ "/ha_entrypoint.sh" ]
|
||||
# 5 Labels #
|
||||
############
|
||||
|
||||
ARG BUILD_ARCH
|
||||
ARG BUILD_DATE
|
||||
ARG BUILD_DESCRIPTION
|
||||
ARG BUILD_NAME
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
![Supports amd64 Architecture][amd64-shield]
|
||||
![Project Maintenance][maintenance-shield]
|
||||
|
||||
Run Claude Desktop in a LinuxServer.io Selkies add-on, with Headroom MCP
|
||||
context compression, RTK Bash-output acceleration, and code-intelligence
|
||||
tooling wired in by default.
|
||||
Run Claude Desktop in a LinuxServer.io Selkies add-on, with Headroom context
|
||||
compression, RTK Bash-output acceleration, and TokenSave semantic code
|
||||
intelligence wired in by default.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -24,15 +24,40 @@ currently does not include Computer Use or dictation.
|
||||
Everything is built around the Claude Desktop app. Claude Code is installed in
|
||||
the same image but is not exposed as a standalone service: Claude Desktop's
|
||||
cowork and dispatch sessions run it internally, and they pick up the shared
|
||||
Claude Code configuration (`~/.claude`), hooks, and MCP servers automatically.
|
||||
Claude Code configuration (`~/.claude`), hooks, MCP servers, permissions, and
|
||||
PATH tools.
|
||||
|
||||
- **Claude Desktop** uses Headroom through its MCP tools.
|
||||
- **Claude Code sessions inside Desktop** get the same MCP servers via
|
||||
`~/.claude.json` and RTK's `PreToolUse` Bash hook via
|
||||
`~/.claude/settings.json`.
|
||||
- **Claude Code sessions inside Desktop** get the same MCP servers, permission
|
||||
mode, and RTK/TokenSave hooks through the shared Claude Code configuration.
|
||||
- PATH-based Claude Code launches are routed through the supervised Headroom
|
||||
proxy when `headroom_wrap_claude_code` is enabled. If a Desktop release calls
|
||||
`/usr/bin/claude` directly, the session remains functional and still has the
|
||||
shared permission mode and Headroom MCP tools, but transparent proxy
|
||||
compression cannot be injected.
|
||||
- The shared `abc` desktop account runs under the configured `PUID`/`PGID`
|
||||
(default `1000:1000`). When `permission_mode: bypass` is selected while
|
||||
`PUID` is `0`, the add-on automatically falls back to UID `1000` before
|
||||
Selkies and Claude Desktop start, because Claude Code refuses bypass mode
|
||||
under an effective root UID.
|
||||
- **gnome-keyring** provides the Secret Service backend Electron needs to
|
||||
persist sign-in and dispatch permission grants across restarts.
|
||||
|
||||
## Optimization layers
|
||||
|
||||
The three bundled optimization tools are complementary:
|
||||
|
||||
- **RTK** rewrites supported Bash commands so Claude receives compact output.
|
||||
- **TokenSave** builds a local semantic graph for explicitly selected code
|
||||
repositories and steers Claude away from repeated Explore/Grep/Read fan-out.
|
||||
- **Headroom** transparently compresses proxied Claude Code traffic and also
|
||||
exposes on-demand compress/retrieve/statistics MCP tools to Claude Desktop.
|
||||
|
||||
TokenSave's complete Claude integration is installed at startup: MCP server,
|
||||
permissions, PreToolUse/UserPromptSubmit/Stop hooks, global prompt rules, and
|
||||
Git synchronization hooks. A repository is indexed only when it is listed in
|
||||
`tokensave_project_paths`; no automatic filesystem scan is performed.
|
||||
|
||||
## Features
|
||||
|
||||
- Claude Desktop in single-app Selkies mode with Home Assistant ingress.
|
||||
@@ -41,65 +66,154 @@ Claude Code configuration (`~/.claude`), hooks, and MCP servers automatically.
|
||||
- Persistent `$HOME` at the configured `data_location` (default `/data/data`),
|
||||
preserving Desktop and Claude Code state across restarts.
|
||||
- Persistent sign-in through a bundled, auto-unlocked gnome-keyring.
|
||||
- Configurable Claude Code permissions: strict prompts, automatic safe-action
|
||||
approval, or explicit full bypass for trusted installations.
|
||||
- Automatic non-root runtime enforcement for bypass mode, including root-console
|
||||
wrapper launches.
|
||||
- Optional runtime Claude Desktop updates from Anthropic's apt repository.
|
||||
- Optional extra apt and pip package installation (pip installs use `uv` for
|
||||
speed).
|
||||
- Baked-in `git`, GitHub CLI (`gh`), and `ripgrep`.
|
||||
- Optional extra apt and pip package installation (pip installs use `uv`).
|
||||
- Baked-in `git`, GitHub CLI (`gh`), `ripgrep`, `jq`, `shellcheck`, `yamllint`,
|
||||
`hadolint`, and `actionlint`.
|
||||
- Custom script support through the repository standard `claude_desktop.sh`.
|
||||
- Bundled optimization tools: Headroom (MCP + local proxy), RTK, tokensave,
|
||||
and Caveman — each individually switchable.
|
||||
- Bundled optimization tools: Headroom, RTK, and TokenSave; Caveman remains
|
||||
available as an opt-in plugin.
|
||||
- Optional Home Assistant MCP bridge so Claude can query and control Home
|
||||
Assistant.
|
||||
- Headroom dashboard exposed on mapped port `8787`.
|
||||
- Independent hourly savings reports for Headroom, RTK, and TokenSave.
|
||||
- `claude-tools-doctor.sh` diagnostics for binaries, routing, hooks, MCP
|
||||
registrations, project indexes, proxy health, permissions, runtime identity,
|
||||
and gains.
|
||||
- Low-power defaults for GPU mapping, Selkies frame rate, and volatile caches.
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------ | ------- | ----------- |
|
||||
| `PUID` / `PGID` | `0` / `0` | Numeric user and group applied by the LinuxServer initialization. |
|
||||
| `PUID` / `PGID` | `1000` / `1000` | Numeric user and group of the shared `abc` desktop account that owns the data location and runs Claude Desktop. In bypass mode, a root `PUID` is automatically replaced at runtime by UID `1000` while the configured group is retained. |
|
||||
| `TZ` | | Optional timezone, for example `Europe/Brussels`. |
|
||||
| `KEYBOARD` | | Optional Selkies keyboard layout. |
|
||||
| `PASSWORD` | | Optional password for direct Selkies ports. |
|
||||
| `DRINODE` | | Optional GPU device override for Selkies. |
|
||||
| `DNS_server` | `8.8.8.8` | DNS server used by the standard DNS module. |
|
||||
| `auto_update` | `true` | Upgrade `claude-desktop` from Anthropic's apt repository at startup. |
|
||||
| `install_headroom` | `true` | Register the Headroom MCP server and run the supervised local proxy/dashboard. |
|
||||
| `install_rtk` | `true` | Configure RTK's Claude Code `PreToolUse` hook. |
|
||||
| `install_tokensave` | `true` | Register the tokensave code-intelligence MCP server for Desktop and Claude Code. |
|
||||
| `install_caveman` | `true` | Install the Caveman Claude Code plugin in the persistent Claude home. |
|
||||
| `permission_mode` | `auto` | Claude Code permission policy: `strict`, `auto`, or `bypass`. |
|
||||
| `install_headroom` | `true` | Register Headroom MCP and run the supervised local proxy. |
|
||||
| `headroom_wrap_claude_code` | `true` | Route PATH-based Claude Code launches through the already-running Headroom proxy. |
|
||||
| `headroom_auto_compress` | `true` | Auto-compress large tool outputs in every Claude Code session via a managed `PostToolUse` hook. |
|
||||
| `expose_headroom_dashboard` | `false` | Bind Headroom to all interfaces. Port `8787/tcp` must also be mapped manually. |
|
||||
| `install_rtk` | `true` | Configure RTK's Claude Code `PreToolUse` Bash hook. |
|
||||
| `install_tokensave` | `true` | Install TokenSave's complete global Claude integration. |
|
||||
| `tokensave_project_paths` | `[]` | Explicit absolute Git repository paths to initialize or sync at startup. |
|
||||
| `install_caveman` | `false` | Install the third-party Caveman Claude Code plugin at startup. |
|
||||
| `enable_tools_health_report` | `true` | Write independent Headroom, RTK, and TokenSave gains to the add-on log hourly. |
|
||||
| `install_github_cli` | `true` | Enable setup checks for the baked-in `git` and `gh` commands. |
|
||||
| `github_token` | | Optional GitHub token used to authenticate `gh` and Git operations. |
|
||||
| `github_username` | | Optional global Git author name. |
|
||||
| `github_email` | | Optional global Git author email. |
|
||||
| `enable_ha_mcp` | `false` | Register Home Assistant's MCP server in Claude (requires `ha_mcp_token`). |
|
||||
| `ha_mcp_url` | `http://homeassistant:8123/mcp_server/sse` | SSE endpoint of Home Assistant's MCP Server integration. |
|
||||
| `ha_mcp_url` | `http://homeassistant:8123/api/mcp` | Streamable HTTP endpoint of Home Assistant's MCP Server integration. |
|
||||
| `ha_mcp_token` | | Home Assistant long-lived access token used by the MCP bridge. |
|
||||
| `enable_ha_api_helper` | `true` | Ship the `ha-cli` Core-API helper and add guidance so Claude can configure Home Assistant without a `/config` mount. |
|
||||
| `additional_apps` | | Comma-separated Debian apt packages to install at startup. |
|
||||
| `additional_pip` | | Comma-separated pip packages installed at startup (via `uv`). |
|
||||
| `data_location` | `/data/data` | Persistent home directory for Claude and tooling. |
|
||||
| `env_vars` | `[]` | Additional environment variables exported inside the container. |
|
||||
|
||||
### Permission modes
|
||||
|
||||
```yaml
|
||||
permission_mode: auto
|
||||
```
|
||||
|
||||
- `strict` keeps Claude Code's normal interactive permission prompts.
|
||||
- `auto` asks Claude Code's automatic permission classifier to approve safe
|
||||
operations while retaining prompts for risky actions. This is the default.
|
||||
- `bypass` disables Claude Code permission checks by using
|
||||
`bypassPermissions` in the shared settings and
|
||||
`--dangerously-skip-permissions` for wrapper-launched sessions.
|
||||
|
||||
Claude Code does not permit bypass mode when its effective UID is `0`. If the
|
||||
add-on is configured with `PUID: 0`, selecting `bypass` runs the shared `abc`
|
||||
runtime account as UID `1000` instead, before storage ownership and Desktop
|
||||
startup. Its configured primary GID is retained, so group-based access to
|
||||
mounted Home Assistant paths remains available. Strict and auto modes keep the
|
||||
configured identity unchanged.
|
||||
|
||||
A root shell invoking `/usr/local/bin/claude` in bypass mode is also dropped to
|
||||
the remapped `abc` account. Directly invoking `/usr/bin/claude` as root still
|
||||
bypasses the add-on wrapper and will be rejected by Claude Code.
|
||||
|
||||
`bypass` gives Claude broad authority over all mounted writable data and every
|
||||
command or credential available inside the add-on. Enable it only in a trusted
|
||||
installation with trusted repositories and mounts. Mounted paths must remain
|
||||
accessible to the effective non-root UID or its retained group.
|
||||
|
||||
### TokenSave project example
|
||||
|
||||
Only repositories listed here are indexed. Paths must be absolute, mounted in
|
||||
the add-on, and resolve to a Git working tree:
|
||||
|
||||
```yaml
|
||||
tokensave_project_paths:
|
||||
- /share/projects/hassio-addons
|
||||
- /share/projects/birdnet-go
|
||||
```
|
||||
|
||||
At startup, an uninitialized repository receives `tokensave init`; an existing
|
||||
index receives an incremental `tokensave sync`. Removing a path from the option
|
||||
stops automatic synchronization but does not delete its `.tokensave` database.
|
||||
Configured repositories are added to Git's `safe.directory` list for the shared
|
||||
runtime user before TokenSave performs repository discovery.
|
||||
|
||||
## Headroom behavior
|
||||
|
||||
When `install_headroom` is enabled, the add-on registers `headroom mcp serve`
|
||||
in Claude Desktop and Claude Code, and starts a supervised local Headroom
|
||||
backend. Claude can use `headroom_compress`, `headroom_retrieve`, and
|
||||
`headroom_stats` through MCP.
|
||||
with the explicit local proxy URL in Claude Desktop and Claude Code, then starts
|
||||
a supervised Headroom backend on `127.0.0.1:8787`.
|
||||
|
||||
Claude Desktop overrides `ANTHROPIC_BASE_URL`, so it is deliberately launched
|
||||
without proxy injection; the MCP integration is the supported path.
|
||||
Claude Desktop overrides `ANTHROPIC_BASE_URL`, so Desktop chat deliberately uses
|
||||
the MCP integration. The `/usr/local/bin/claude` wrapper routes PATH-based Claude
|
||||
Code sessions through `headroom wrap claude --no-proxy`, reusing the supervised
|
||||
backend without starting a second proxy.
|
||||
|
||||
The Headroom dashboard is available at:
|
||||
With `headroom_auto_compress` enabled (the default), a managed Claude Code
|
||||
`PostToolUse` hook additionally compresses large `Bash`/`Grep`/`Glob`/`WebFetch`
|
||||
outputs (over ~4000 characters) in **every** session type — terminal, Desktop
|
||||
cowork, dispatch, and cron — without the model having to remember to call the
|
||||
MCP tools. The original output is kept in Headroom's local store for one hour
|
||||
and can always be recovered with `mcp__headroom__headroom_retrieve` using the
|
||||
hash printed in the compression marker. Error text (`stderr`) is never
|
||||
compressed, and plain prose passes through unchanged; the savings come from
|
||||
structured output such as JSON dumps, search results, and logs.
|
||||
|
||||
```text
|
||||
http://<home-assistant-host>:8787/dashboard
|
||||
The dashboard is disabled externally by default. To expose it:
|
||||
|
||||
1. Set `expose_headroom_dashboard: true`.
|
||||
2. Map `8787/tcp` in the add-on **Network** section.
|
||||
3. Open `http://<home-assistant-host>:8787/dashboard`.
|
||||
|
||||
The dashboard is unauthenticated. Do not publish this port to the public
|
||||
internet.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Run the following inside the add-on through a custom script or container console:
|
||||
|
||||
```bash
|
||||
claude-tools-doctor.sh
|
||||
```
|
||||
|
||||
through the default `8787/tcp` port mapping. The dashboard is unauthenticated
|
||||
and is reachable wherever Home Assistant publishes that port, so treat it as
|
||||
sensitive: do not expose it directly to the public internet, and unmap the port
|
||||
in the add-on **Network** section if you do not want it reachable at all.
|
||||
The report checks the tool binaries, configuration switches, configured and
|
||||
effective runtime identities, redacted MCP registrations, Claude hooks,
|
||||
permission mode, Headroom health, TokenSave indexes, routing, and recorded
|
||||
savings. It never prints MCP environment values because the Home Assistant MCP
|
||||
entry can contain a long-lived token.
|
||||
|
||||
The hourly report can also be invoked manually:
|
||||
|
||||
```bash
|
||||
claude-gains-report.sh
|
||||
```
|
||||
|
||||
## Home Assistant MCP bridge
|
||||
|
||||
@@ -115,6 +229,45 @@ 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
|
||||
|
||||
The add-on includes the repository standard custom-script executor. On first
|
||||
@@ -129,9 +282,12 @@ Persistent state is stored in the configured `data_location` (default
|
||||
|
||||
- Claude Desktop sign-in: `~/.config/Claude` (token encrypted via
|
||||
gnome-keyring; keyring DB in `~/.local/share/keyrings`)
|
||||
- Claude Code settings, hooks, sessions, and plugins: `~/.claude`
|
||||
- Headroom, RTK, and tokensave user state: their standard paths below the
|
||||
- Claude Code settings, hooks, sessions, plugins, and permission mode:
|
||||
`~/.claude`
|
||||
- Headroom, RTK, and TokenSave user state: their standard paths below the
|
||||
shared home
|
||||
- TokenSave repository indexes: `.tokensave/` inside each explicitly configured
|
||||
project
|
||||
|
||||
Volatile cache data is redirected to `/tmp/cache` through `$XDG_CACHE_HOME` and
|
||||
`$HOME/.cache`.
|
||||
|
||||
@@ -2,7 +2,7 @@ arch:
|
||||
- aarch64
|
||||
- amd64
|
||||
audio: true
|
||||
description: "Claude Desktop with Headroom MCP context compression and RTK acceleration"
|
||||
description: "Claude Desktop with Headroom, RTK, and TokenSave optimization"
|
||||
devices:
|
||||
- /dev/dri
|
||||
- /dev/dri/card0
|
||||
@@ -13,13 +13,14 @@ environment:
|
||||
AUTO_GPU: "1"
|
||||
FM_HOME: /data/data
|
||||
HOME: /data/data
|
||||
PGID: "0"
|
||||
PUID: "0"
|
||||
PGID: "1000"
|
||||
PUID: "1000"
|
||||
SELKIES_FRAMERATE: "30"
|
||||
START_DOCKER: "false"
|
||||
TITLE: Claude Desktop
|
||||
image: ghcr.io/alexbelgium/claude_desktop-{arch}
|
||||
ingress: true
|
||||
ingress_port: 3001
|
||||
init: false
|
||||
hassio_api: true
|
||||
hassio_role: manager
|
||||
@@ -34,9 +35,9 @@ name: Claude Desktop
|
||||
options:
|
||||
env_vars: []
|
||||
DNS_server: 8.8.8.8
|
||||
PGID: 1000
|
||||
PUID: 1000
|
||||
data_location: /data/data
|
||||
PGID: 0
|
||||
PUID: 0
|
||||
additional_apps: ""
|
||||
additional_pip: ""
|
||||
auto_update: true
|
||||
@@ -44,21 +45,28 @@ options:
|
||||
enable_ha_mcp: false
|
||||
ha_mcp_url: http://homeassistant:8123/api/mcp
|
||||
ha_mcp_token: ""
|
||||
enable_ha_api_helper: true
|
||||
github_token: ""
|
||||
github_username: ""
|
||||
install_caveman: true
|
||||
enable_tools_health_report: true
|
||||
expose_headroom_dashboard: false
|
||||
headroom_auto_compress: true
|
||||
headroom_wrap_claude_code: true
|
||||
install_caveman: false
|
||||
install_github_cli: true
|
||||
install_headroom: true
|
||||
install_rtk: true
|
||||
install_tokensave: true
|
||||
permission_mode: auto
|
||||
tokensave_project_paths: []
|
||||
panel_admin: false
|
||||
panel_icon: mdi:robot-happy
|
||||
ports:
|
||||
3001/tcp: null
|
||||
8787/tcp: 8787
|
||||
8787/tcp: null
|
||||
ports_description:
|
||||
3001/tcp: Claude Desktop web interface
|
||||
8787/tcp: Headroom dashboard and proxy
|
||||
8787/tcp: Optional Headroom dashboard and proxy
|
||||
privileged:
|
||||
- SYS_ADMIN
|
||||
- DAC_READ_SEARCH
|
||||
@@ -76,21 +84,33 @@ schema:
|
||||
TZ: match([A-Z][a-z]*./[A-Z][a-z]*.)?
|
||||
additional_apps: str?
|
||||
additional_pip: str?
|
||||
auto_update: bool?
|
||||
cifsdomain: str?
|
||||
cifspassword: str?
|
||||
cifsusername: str?
|
||||
localdisks: str?
|
||||
networkdisks: str?
|
||||
github_email: str?
|
||||
enable_ha_mcp: bool?
|
||||
ha_mcp_url: str?
|
||||
ha_mcp_token: password?
|
||||
enable_ha_api_helper: bool?
|
||||
github_token: password?
|
||||
github_username: str?
|
||||
enable_tools_health_report: bool
|
||||
expose_headroom_dashboard: bool
|
||||
headroom_auto_compress: bool?
|
||||
headroom_wrap_claude_code: bool
|
||||
install_caveman: bool
|
||||
install_github_cli: bool
|
||||
install_headroom: bool
|
||||
install_rtk: bool
|
||||
install_tokensave: bool
|
||||
permission_mode: list(strict|auto|bypass)
|
||||
tokensave_project_paths:
|
||||
- str
|
||||
slug: claude_desktop
|
||||
tmpfs: true
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "1.17"
|
||||
version: "1.31"
|
||||
video: true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Hourly rtk + headroom token-savings report to the add-on log (heartbeat + gains).
|
||||
# Hourly RTK + Headroom + TokenSave savings report to the add-on log.
|
||||
# Seeded to /data/data/crontabs/root by init-crontab-config and run by svc-cron; edit the
|
||||
# persistent copy to customize. Output goes to /proc/1/fd/1 so it shows in the add-on log.
|
||||
0 * * * * /usr/local/bin/claude-gains-report.sh > /proc/1/fd/1 2>&1
|
||||
|
||||
@@ -3,9 +3,35 @@
|
||||
# shellcheck disable=SC2046
|
||||
set -e
|
||||
|
||||
# Define user
|
||||
PUID=$(bashio::config "PUID")
|
||||
PGID=$(bashio::config "PGID")
|
||||
# Align the shared desktop user (abc) with the configured PUID/PGID before any storage is
|
||||
# chowned and before any service or s6-setuidgid call resolves abc. The base image's
|
||||
# init-adduser applies the same remap, but it runs after cont-init, so doing it here first is
|
||||
# what lets the tokensave/rtk/git setup in the 8x scripts run under the final identity.
|
||||
PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else echo '1000'; fi)"
|
||||
PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '1000'; fi)"
|
||||
|
||||
# Claude Code refuses bypass-permissions mode under an effective root UID, so bypass mode
|
||||
# always needs a non-root desktop user.
|
||||
if [ "$(bashio::config 'permission_mode')" = "bypass" ] && [ "$PUID" -eq 0 ]; then
|
||||
bashio::log.warning "permission_mode: bypass cannot run Claude Code as root; using UID 1000 instead of the configured PUID 0"
|
||||
PUID=1000
|
||||
fi
|
||||
|
||||
groupmod -o -g "$PGID" abc 2> /dev/null || true
|
||||
usermod -o -u "$PUID" abc 2> /dev/null || true
|
||||
if [ "$(id -u abc)" -ne "$PUID" ] || [ "$(id -g abc)" -ne "$PGID" ]; then
|
||||
PUID="$(id -u abc)"
|
||||
PGID="$(id -g abc)"
|
||||
bashio::log.warning "Unable to remap the abc desktop user; continuing with its current identity ${PUID}:${PGID}"
|
||||
fi
|
||||
|
||||
# The base image's init-adduser reads PUID/PGID from the raw add-on options (default 0) and
|
||||
# runs mid-startup, racing the services. Pin it to the effective identity chosen above so it
|
||||
# can never remap abc away from the ownership applied below.
|
||||
ADDUSER_RUN="/etc/s6-overlay/s6-rc.d/init-adduser/run"
|
||||
if [ -f "$ADDUSER_RUN" ]; then
|
||||
sed -i "s|^PUID=.*|PUID=${PUID}|;s|^PGID=.*|PGID=${PGID}|" "$ADDUSER_RUN"
|
||||
fi
|
||||
|
||||
# Check data location
|
||||
LOCATION="$(bashio::config 'data_location')"
|
||||
@@ -59,10 +85,23 @@ printf "%s" "$LOCATION" > "$S6_ENVDIR/HOME"
|
||||
printf "%s" "$LOCATION" > "$S6_ENVDIR/FM_HOME"
|
||||
printf "%s" "/tmp/cache" > "$S6_ENVDIR/XDG_CACHE_HOME"
|
||||
printf "%s" "$XDG_RUNTIME_DIR" > "$S6_ENVDIR/XDG_RUNTIME_DIR"
|
||||
grep -qxF "export HOME=\"$LOCATION\"" ~/.bashrc 2>/dev/null || {
|
||||
# Re-derived on every boot rather than gated on a "does it already say $LOCATION" grep: that
|
||||
# guard only ever recognized the CURRENT $LOCATION, so a user who changed data_location and
|
||||
# later changed it back left two stale HOME/FM_HOME exports in ~/.bashrc, with the last one
|
||||
# (not necessarily the correct one) winning for every interactive shell. The marker makes this
|
||||
# idempotent regardless of how many times $LOCATION has changed: strip any previously managed
|
||||
# block, then append one that reflects the current value.
|
||||
BASHRC_HOME_BEGIN="# --- BEGIN ADDON HOME (managed) ---"
|
||||
BASHRC_HOME_END="# --- END ADDON HOME (managed) ---"
|
||||
if [ -f ~/.bashrc ]; then
|
||||
sed -i "/^${BASHRC_HOME_BEGIN}\$/,/^${BASHRC_HOME_END}\$/d" ~/.bashrc
|
||||
fi
|
||||
{
|
||||
printf "%s\n" "$BASHRC_HOME_BEGIN"
|
||||
printf "%s\n" "export HOME=\"$LOCATION\""
|
||||
printf "%s\n" "export FM_HOME=\"$LOCATION\""
|
||||
printf "%s\n" "export XDG_CACHE_HOME=\"/tmp/cache\""
|
||||
printf "%s\n" "$BASHRC_HOME_END"
|
||||
} >> ~/.bashrc
|
||||
|
||||
bashio::log.info "Creating $LOCATION"
|
||||
@@ -70,6 +109,11 @@ mkdir -p "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR"
|
||||
chmod 755 /tmp/cache
|
||||
chmod 700 "$XDG_RUNTIME_DIR"
|
||||
|
||||
# /tmp is a tmpfs and Xorg runs as the non-root abc user, which cannot create the X11 socket
|
||||
# directory itself (_XSERVTransmkdir: euid != 0). Pre-create it with the standard sticky mode.
|
||||
mkdir -p /tmp/.X11-unix
|
||||
chmod 1777 /tmp/.X11-unix
|
||||
|
||||
# Pre-create the Selkies joystick log so the base image's "chmod 777 /tmp/selkies*"
|
||||
# calls (in init-selkies-config and svc-de) never fail on an empty glob.
|
||||
touch /tmp/selkies_js.log
|
||||
@@ -81,7 +125,7 @@ fi
|
||||
ln -sfn /tmp/cache "$LOCATION/.cache"
|
||||
|
||||
bashio::log.info "Setting ownership to $PUID:$PGID"
|
||||
chown -R "$PUID":"$PGID" "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR"
|
||||
chown -R "${PUID}:${PGID}" "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR" /data
|
||||
chmod -R 700 "$LOCATION"
|
||||
|
||||
# The base init-selkies-config script overrides XDG_RUNTIME_DIR to $HOME/.XDG, which lands
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
|
||||
if bashio::config.true 'auto_update'; then
|
||||
bashio::log.info "Checking for Claude Desktop updates..."
|
||||
if apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null && apt-get install -y --only-upgrade claude-desktop &> /dev/null; then
|
||||
bashio::log.info "Claude Desktop version: $(dpkg-query -W -f='${Version}' claude-desktop)"
|
||||
else
|
||||
bashio::log.warning "Update check failed (offline?), keeping current version"
|
||||
fi
|
||||
bashio::log.info "Checking for Claude Desktop updates..."
|
||||
if apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null && apt-get install -y --only-upgrade claude-desktop &> /dev/null; then
|
||||
bashio::log.info "Claude Desktop version: $(dpkg-query -W -f='${Version}' claude-desktop)"
|
||||
else
|
||||
bashio::log.warning "Update check failed (offline?), keeping current version"
|
||||
fi
|
||||
|
||||
42
claude_desktop/rootfs/etc/cont-init.d/81-tokensave_repositories.sh
Executable file
42
claude_desktop/rootfs/etc/cont-init.d/81-tokensave_repositories.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
if ! bashio::config.true 'install_tokensave' || ! command -v git > /dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
declare -A REPOS_SEEN=()
|
||||
# bashio::config prints its result without a trailing newline, so the last record arrives
|
||||
# with read returning non-zero; the extra test keeps that final path in the loop.
|
||||
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
|
||||
configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}"
|
||||
configured_path="${configured_path%"${configured_path##*[![:space:]]}"}"
|
||||
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
case "$configured_path" in
|
||||
/*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
[ -d "$configured_path" ] || continue
|
||||
|
||||
# The one-shot safe.directory override is used only to discover the repository root.
|
||||
# Persist the resolved root in the shared runtime user's Git config before 82-claude_tools.sh
|
||||
# performs normal repository detection, avoiding Git's dubious-ownership rejection.
|
||||
repo_root="$(s6-setuidgid abc env HOME="$HOME" \
|
||||
git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
|
||||
[ -n "$repo_root" ] && [ "$repo_root" != "/" ] || continue
|
||||
[[ -z "${REPOS_SEEN[$repo_root]:-}" ]] || continue
|
||||
REPOS_SEEN[$repo_root]=1
|
||||
|
||||
if ! s6-setuidgid abc env HOME="$HOME" git config --global --get-all safe.directory \
|
||||
| grep -Fxq -- "$repo_root"; then
|
||||
s6-setuidgid abc env HOME="$HOME" git config --global --add safe.directory "$repo_root"
|
||||
bashio::log.info "Marked TokenSave repository as safe for Git: ${repo_root}"
|
||||
fi
|
||||
# bashio::config prints list options one entry per line ("null" when the key is absent);
|
||||
# bashio::config.array only exists in the repo's standalone bashio, not in the real bashio here.
|
||||
done < <(bashio::config 'tokensave_project_paths')
|
||||
@@ -3,19 +3,24 @@
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else echo '0'; fi)"
|
||||
PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '0'; fi)"
|
||||
# 20-folders.sh already remapped abc to the effective runtime identity (never root in bypass
|
||||
# mode), so follow abc instead of re-reading the raw PUID/PGID options here.
|
||||
RUNTIME_UID="$(id -u abc)"
|
||||
RUNTIME_GID="$(id -g abc)"
|
||||
mkdir -p "$HOME/.claude"
|
||||
|
||||
run_as_runtime_user() {
|
||||
s6-setuidgid abc env HOME="$HOME" "$@"
|
||||
}
|
||||
|
||||
CLAUDE_DESKTOP_COMMAND_FILE="/tmp/claude-desktop-command"
|
||||
DEFAULT_CLAUDE_DESKTOP_COMMAND='claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=gnome-libsecret'
|
||||
printf '%s\n' "$DEFAULT_CLAUDE_DESKTOP_COMMAND" > "$CLAUDE_DESKTOP_COMMAND_FILE"
|
||||
|
||||
# headroom's "wrap"/proxy routing works by setting ANTHROPIC_BASE_URL, which the Claude Desktop
|
||||
# Electron app force-overrides to the production endpoint (headroom #869), so transparent
|
||||
# compression cannot be applied to the desktop launch. The integration that does work with
|
||||
# Claude Desktop is headroom's MCP server, which exposes the headroom_compress/headroom_retrieve/
|
||||
# headroom_stats tools inside the app.
|
||||
# Headroom's proxy routing works by setting ANTHROPIC_BASE_URL, which the Claude Desktop
|
||||
# Electron app force-overrides to the production endpoint (headroom #869). Desktop therefore
|
||||
# uses Headroom's MCP tools. Claude Code launches that resolve `claude` through PATH use the
|
||||
# add-on's /usr/local/bin/claude wrapper and can be transparently proxied when enabled.
|
||||
#
|
||||
# Register the add-on-managed MCP servers (headroom, tokensave, homeassistant) in both Claude
|
||||
# Desktop's config and Claude Code's user config (used by Desktop cowork/dispatch sessions).
|
||||
@@ -38,10 +43,18 @@ TOKENSAVE_ENABLED=false
|
||||
if bashio::config.true 'install_tokensave'; then
|
||||
if command -v tokensave &> /dev/null; then
|
||||
TOKENSAVE_ENABLED=true
|
||||
bashio::log.info "tokensave $(tokensave --version 2> /dev/null || true) available; registering the tokensave MCP server"
|
||||
bashio::log.info "tokensave $(tokensave --version 2> /dev/null || true) available; configuring the complete Claude Code integration"
|
||||
# The upstream installer adds the MCP entry, PreToolUse/UserPromptSubmit/Stop hooks,
|
||||
# MCP permissions, global CLAUDE.md rules, and the global post-commit/checkout sync hook.
|
||||
run_as_runtime_user tokensave install --agent claude --git-hook yes \
|
||||
|| bashio::log.warning "tokensave Claude Code integration setup failed"
|
||||
else
|
||||
bashio::log.warning "tokensave is not available"
|
||||
fi
|
||||
elif command -v tokensave &> /dev/null; then
|
||||
bashio::log.info "Removing the tokensave Claude Code integration"
|
||||
run_as_runtime_user tokensave uninstall --agent claude \
|
||||
|| bashio::log.warning "tokensave Claude Code integration removal failed"
|
||||
fi
|
||||
|
||||
HA_MCP_ENABLED=false
|
||||
@@ -63,6 +76,7 @@ if bashio::config.true 'enable_ha_mcp'; then
|
||||
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)" \
|
||||
@@ -80,7 +94,15 @@ MANAGED_BASENAMES = {
|
||||
|
||||
desired = {}
|
||||
if os.environ["HEADROOM_ENABLED"] == "true":
|
||||
desired["headroom"] = {"command": os.environ["HEADROOM_BIN"], "args": ["mcp", "serve"]}
|
||||
desired["headroom"] = {
|
||||
"command": os.environ["HEADROOM_BIN"],
|
||||
"args": ["mcp", "serve", "--proxy-url", "http://127.0.0.1:8787"],
|
||||
# The MCP server is a separate process from the svc-headroom proxy longrun and does
|
||||
# not inherit its HF_HOME export, so Kompress falls back to the default (tmpfs, wiped
|
||||
# every restart) cache dir, never finds the model, and silently no-ops every
|
||||
# compression request. Point it at the same persistent cache the proxy warms.
|
||||
"env": {"HF_HOME": os.environ["HEADROOM_HF_HOME"]},
|
||||
}
|
||||
if os.environ["TOKENSAVE_ENABLED"] == "true":
|
||||
desired["tokensave"] = {"command": os.environ["TOKENSAVE_BIN"], "args": ["serve"]}
|
||||
if os.environ["HA_MCP_ENABLED"] == "true":
|
||||
@@ -98,6 +120,7 @@ if os.environ["HA_MCP_ENABLED"] == "true":
|
||||
# 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
|
||||
@@ -106,6 +129,7 @@ def is_managed(name, entry):
|
||||
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:
|
||||
@@ -145,12 +169,125 @@ for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_C
|
||||
path.chmod(0o600)
|
||||
PY
|
||||
|
||||
# Guide Claude to actually use the headroom compression tools so the MCP integration produces
|
||||
# real savings (otherwise the tools sit unused and `headroom savings` stays empty). Managed,
|
||||
# idempotent block appended to the user's global CLAUDE.md; removed when headroom is disabled.
|
||||
# Initialize or incrementally sync only explicitly configured repositories. TokenSave deliberately
|
||||
# requires one-time per-project opt-in; an empty list therefore has no startup or storage cost.
|
||||
if $TOKENSAVE_ENABLED; then
|
||||
declare -A TOKENSAVE_REPOS_SEEN=()
|
||||
# bashio::config prints its result without a trailing newline, so the last record arrives
|
||||
# with read returning non-zero; the extra test keeps that final path in the loop.
|
||||
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
|
||||
# Trim surrounding whitespace while preserving spaces inside paths.
|
||||
configured_path="${configured_path#"${configured_path%%[![:space:]]*}"}"
|
||||
configured_path="${configured_path%"${configured_path##*[![:space:]]}"}"
|
||||
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
case "$configured_path" in
|
||||
/*) ;;
|
||||
*)
|
||||
bashio::log.warning "Skipping non-absolute tokensave_project_paths entry: ${configured_path}"
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
if [ ! -d "$configured_path" ]; then
|
||||
bashio::log.warning "Skipping missing TokenSave project path: ${configured_path}"
|
||||
continue
|
||||
fi
|
||||
|
||||
repo_root="$(git -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
|
||||
if [ -z "$repo_root" ] || [ "$repo_root" = "/" ]; then
|
||||
bashio::log.warning "Skipping TokenSave path that is not a supported Git repository: ${configured_path}"
|
||||
continue
|
||||
fi
|
||||
if [[ -n "${TOKENSAVE_REPOS_SEEN[$repo_root]:-}" ]]; then
|
||||
continue
|
||||
fi
|
||||
TOKENSAVE_REPOS_SEEN[$repo_root]=1
|
||||
|
||||
bashio::log.info "Preparing TokenSave index: ${repo_root}"
|
||||
# Prepare the per-repo semantic graph defensively so a hard add-on stop or storage
|
||||
# hiccup can never leave a broken index that fails every subsequent boot:
|
||||
# * a startup-scoped flock serializes against an overlapping restart (and any git
|
||||
# post-commit/checkout sync hook that fires mid-boot); waits up to 60s for the
|
||||
# other writer to finish rather than silently skipping, since a held lock clears
|
||||
# itself the moment its holder exits or dies (the kernel releases flock on exit);
|
||||
# * an existing index is refreshed with a cheap incremental `sync`, retried a few
|
||||
# times because SQLITE_BUSY under lock contention is transient, not corruption;
|
||||
# * quarantine is reserved for sync failures whose stderr actually names database
|
||||
# corruption (SQLite's own "malformed"/"not a database"/"disk image" wording) or
|
||||
# a half-written index from an interrupted `init` (sentinel-flagged). Any other
|
||||
# failure (permissions, disk full, missing binary, ...) leaves the existing index
|
||||
# untouched and simply retries on the next start — corruption should self-heal,
|
||||
# a transient environment problem should not nuke a healthy graph;
|
||||
# * `init` is bracketed by a sentinel file so an interrupted full build is detected
|
||||
# as incomplete on the next start and rebuilt rather than trusted.
|
||||
# All file operations run as the abc runtime user because the repo `.tokensave`
|
||||
# directory is not covered by this script's final ownership pass.
|
||||
# shellcheck disable=SC2016 # single-quoted on purpose: $1/$db/etc. expand in the abc shell
|
||||
run_as_runtime_user bash -c '
|
||||
set -o pipefail
|
||||
repo_root="$1"
|
||||
ts_dir="$repo_root/.tokensave"
|
||||
db="$ts_dir/tokensave.db"
|
||||
lock="$ts_dir/.startup.lock"
|
||||
initflag="$ts_dir/.init-incomplete"
|
||||
mkdir -p "$ts_dir"
|
||||
exec 9>"$lock"
|
||||
if ! flock -w 60 9; then
|
||||
echo "TokenSave: index still locked for $repo_root after 60s; skipping startup sync" >&2
|
||||
exit 0
|
||||
fi
|
||||
is_corruption() {
|
||||
printf "%s" "$1" | grep -qiE "malformed|not a database|file is encrypted|disk image|database.*corrupt"
|
||||
}
|
||||
quarantine() {
|
||||
stamp="$(date +%Y%m%d-%H%M%S)"
|
||||
bdir="$ts_dir/corrupt-$stamp"
|
||||
mkdir -p "$bdir"
|
||||
for f in "$db" "$db-wal" "$db-shm"; do
|
||||
[ -e "$f" ] && mv -f "$f" "$bdir/" 2>/dev/null || true
|
||||
done
|
||||
echo "TokenSave: quarantined suspect index to $bdir" >&2
|
||||
}
|
||||
if [ -f "$db" ] && [ ! -f "$initflag" ]; then
|
||||
attempt=1
|
||||
while :; do
|
||||
sync_err="$(tokensave sync "$repo_root" 2>&1 1>/dev/null)" && exit 0
|
||||
[ "$attempt" -ge 3 ] && break
|
||||
echo "TokenSave: sync attempt $attempt failed for $repo_root; retrying" >&2
|
||||
attempt=$((attempt + 1))
|
||||
sleep 2
|
||||
done
|
||||
if is_corruption "$sync_err"; then
|
||||
echo "TokenSave: sync failed after retries for $repo_root (corruption detected); rebuilding index" >&2
|
||||
quarantine
|
||||
else
|
||||
echo "TokenSave: sync failed after retries for $repo_root (no corruption signature); leaving index in place, will retry next start" >&2
|
||||
echo "TokenSave: last sync error: $sync_err" >&2
|
||||
exit 1
|
||||
fi
|
||||
elif [ -f "$db" ]; then
|
||||
echo "TokenSave: previous init did not finish for $repo_root; rebuilding index" >&2
|
||||
quarantine
|
||||
fi
|
||||
: > "$initflag"
|
||||
tokensave init "$repo_root" && { rm -f "$initflag"; exit 0; }
|
||||
echo "TokenSave: init failed for $repo_root; will retry on next start" >&2
|
||||
exit 1
|
||||
' _ "$repo_root" \
|
||||
|| bashio::log.warning "TokenSave preparation failed for ${repo_root}"
|
||||
# bashio::config prints list options one entry per line ("null" when the key is absent);
|
||||
# bashio::config.array only exists in the repo's standalone bashio, not in the real bashio here.
|
||||
done < <(bashio::config 'tokensave_project_paths')
|
||||
fi
|
||||
|
||||
# Guide Claude to actually use the Headroom compression tools so the MCP integration produces
|
||||
# real savings when transparent proxying is unavailable. Managed, idempotent block appended to
|
||||
# the user's global CLAUDE.md; removed when Headroom is disabled.
|
||||
CLAUDE_MD="$HOME/.claude/CLAUDE.md"
|
||||
HEADROOM_GUIDE_BEGIN="<!-- BEGIN headroom (managed by claude_desktop addon) -->"
|
||||
if bashio::config.true 'install_headroom'; then
|
||||
if $HEADROOM_ENABLED; then
|
||||
mkdir -p "$(dirname "$CLAUDE_MD")"
|
||||
if ! { [ -f "$CLAUDE_MD" ] && grep -qF "$HEADROOM_GUIDE_BEGIN" "$CLAUDE_MD"; }; then
|
||||
bashio::log.info "Adding headroom usage guidance to CLAUDE.md"
|
||||
@@ -191,16 +328,213 @@ if new != text:
|
||||
PY
|
||||
fi
|
||||
|
||||
# Route every Claude Code session through the Headroom proxy via the `env` block in the user's
|
||||
# ~/.claude/settings.json. Claude Code writes settings `env` entries into the process
|
||||
# environment at startup, replacing inherited values — this is the only supported way to reach
|
||||
# Desktop cowork/local-agent-mode sessions, which spawn the bundled CLI at an absolute path
|
||||
# (bypassing the PATH wrapper) with ANTHROPIC_BASE_URL pinned to the production endpoint
|
||||
# (headroom #869). Managed-value semantics: only set or remove the variable when it is absent
|
||||
# or already equals the add-on-managed proxy URL, so a user-customized endpoint is never
|
||||
# clobbered. The svc-headroom longrun is s6-supervised, so a crashed proxy restarts within
|
||||
# seconds; the terminal wrapper's per-launch health check remains as an extra safety net.
|
||||
if $HEADROOM_ENABLED && bashio::config.true 'headroom_wrap_claude_code'; then
|
||||
HEADROOM_ROUTE_ACTION="add"
|
||||
else
|
||||
HEADROOM_ROUTE_ACTION="remove"
|
||||
fi
|
||||
HEADROOM_ROUTE_ACTION="$HEADROOM_ROUTE_ACTION" python3 - <<'PY' || bashio::log.warning "Unable to manage the Claude Code proxy routing env"
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
MANAGED_URL = "http://127.0.0.1:8787"
|
||||
|
||||
path = Path.home() / ".claude" / "settings.json"
|
||||
try:
|
||||
data = json.loads(path.read_text()) if path.exists() else {}
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
except Exception:
|
||||
if path.exists():
|
||||
path.rename(path.with_suffix(path.suffix + ".bak"))
|
||||
data = {}
|
||||
|
||||
env = data.get("env")
|
||||
if not isinstance(env, dict):
|
||||
env = {}
|
||||
current = env.get("ANTHROPIC_BASE_URL")
|
||||
changed = False
|
||||
|
||||
if os.environ["HEADROOM_ROUTE_ACTION"] == "add":
|
||||
if current is None or current == MANAGED_URL:
|
||||
if current != MANAGED_URL:
|
||||
env["ANTHROPIC_BASE_URL"] = MANAGED_URL
|
||||
changed = True
|
||||
else:
|
||||
print(f"Claude settings env already sets ANTHROPIC_BASE_URL={current}; leaving it untouched")
|
||||
elif current == MANAGED_URL:
|
||||
del env["ANTHROPIC_BASE_URL"]
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
if env:
|
||||
data["env"] = env
|
||||
elif "env" in data:
|
||||
del data["env"]
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
PY
|
||||
|
||||
# Compress large tool outputs automatically in every Claude Code session via a managed
|
||||
# PostToolUse hook (settings.json hooks apply to terminal, cowork, dispatch and cron sessions
|
||||
# alike). Desktop-spawned sessions pin ANTHROPIC_BASE_URL to the production endpoint
|
||||
# (headroom #869) so the proxy never sees their traffic, and the CLAUDE.md guidance above only
|
||||
# helps when the model remembers to call the MCP tools. The hook closes that gap: outputs over
|
||||
# ~4000 chars from Bash/Grep/Glob/WebFetch are compressed with Headroom's rule-based pipeline
|
||||
# and swapped in through hookSpecificOutput.updatedToolOutput, with the original kept in the
|
||||
# shared CCR store so the model can fetch it back with mcp__headroom__headroom_retrieve. The
|
||||
# script fails open (any error leaves the tool output untouched) and its --self-test gate
|
||||
# keeps a broken interpreter path from registering a hook that would warn on every tool call.
|
||||
HEADROOM_HOOK_CMD="/usr/local/bin/headroom-posttooluse-compress.py"
|
||||
HEADROOM_HOOK_ACTION="remove"
|
||||
if $HEADROOM_ENABLED && bashio::config.true 'headroom_auto_compress'; then
|
||||
if run_as_runtime_user "$HEADROOM_HOOK_CMD" --self-test; then
|
||||
HEADROOM_HOOK_ACTION="add"
|
||||
bashio::log.info "Registering the Headroom PostToolUse auto-compression hook"
|
||||
else
|
||||
bashio::log.warning "headroom-posttooluse-compress.py --self-test failed; not registering the auto-compression hook"
|
||||
fi
|
||||
fi
|
||||
HEADROOM_HOOK_ACTION="$HEADROOM_HOOK_ACTION" HEADROOM_HOOK_CMD="$HEADROOM_HOOK_CMD" \
|
||||
HEADROOM_HOOK_MATCHER="Bash|Grep|Glob|WebFetch" \
|
||||
python3 - <<'PY' || bashio::log.warning "Unable to manage the Headroom auto-compression hook"
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
action = os.environ["HEADROOM_HOOK_ACTION"]
|
||||
command = os.environ["HEADROOM_HOOK_CMD"]
|
||||
matcher = os.environ["HEADROOM_HOOK_MATCHER"]
|
||||
|
||||
path = Path.home() / ".claude" / "settings.json"
|
||||
original = path.read_text() if path.exists() else None
|
||||
try:
|
||||
data = json.loads(original) if original is not None else {}
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
except Exception:
|
||||
if action != "add":
|
||||
raise SystemExit(0)
|
||||
path.rename(path.with_suffix(path.suffix + ".bak"))
|
||||
original = None
|
||||
data = {}
|
||||
|
||||
hooks = data.get("hooks") if isinstance(data.get("hooks"), dict) else {}
|
||||
entries = hooks.get("PostToolUse") if isinstance(hooks.get("PostToolUse"), list) else []
|
||||
|
||||
# Strip the managed command everywhere first, then re-append when enabled: the same pass
|
||||
# handles removal, de-duplication, and matcher migration on version upgrades. The final
|
||||
# text comparison keeps the write idempotent across boots.
|
||||
filtered = []
|
||||
for entry in entries:
|
||||
if not isinstance(entry, dict) or not isinstance(entry.get("hooks"), list):
|
||||
filtered.append(entry)
|
||||
continue
|
||||
kept = [
|
||||
item
|
||||
for item in entry["hooks"]
|
||||
if not (isinstance(item, dict) and item.get("command") == command)
|
||||
]
|
||||
if len(kept) != len(entry["hooks"]):
|
||||
if not kept:
|
||||
continue
|
||||
entry = dict(entry)
|
||||
entry["hooks"] = kept
|
||||
filtered.append(entry)
|
||||
entries = filtered
|
||||
|
||||
if action == "add":
|
||||
entries.append({"matcher": matcher, "hooks": [{"type": "command", "command": command}]})
|
||||
|
||||
if entries:
|
||||
hooks["PostToolUse"] = entries
|
||||
else:
|
||||
hooks.pop("PostToolUse", None)
|
||||
if hooks:
|
||||
data["hooks"] = hooks
|
||||
else:
|
||||
data.pop("hooks", None)
|
||||
|
||||
serialized = json.dumps(data, indent=2) + "\n"
|
||||
if serialized != original:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(serialized)
|
||||
PY
|
||||
|
||||
# Tell Claude Code that it can configure Home Assistant over the Core API via the shipped
|
||||
# `ha-cli` helper (no /config filesystem mount needed). Managed, idempotent block appended to
|
||||
# the user's global CLAUDE.md; removed when the helper is disabled. Mirrors the headroom block.
|
||||
HA_HELPER_GUIDE_BEGIN="<!-- BEGIN ha-api-helper (managed by claude_desktop addon) -->"
|
||||
if bashio::config.true 'enable_ha_api_helper'; then
|
||||
mkdir -p "$(dirname "$CLAUDE_MD")"
|
||||
if ! { [ -f "$CLAUDE_MD" ] && grep -qF "$HA_HELPER_GUIDE_BEGIN" "$CLAUDE_MD"; }; then
|
||||
bashio::log.info "Adding Home Assistant API helper guidance to CLAUDE.md"
|
||||
{
|
||||
[ -s "$CLAUDE_MD" ] && printf '\n'
|
||||
cat <<'MD'
|
||||
<!-- BEGIN ha-api-helper (managed by claude_desktop addon) -->
|
||||
## Configuring Home Assistant
|
||||
|
||||
You can configure this Home Assistant instance through its Core API using the `ha-cli`
|
||||
command (on `PATH`). It authenticates automatically with the add-on's `$SUPERVISOR_TOKEN`,
|
||||
so no token setup is needed. There is **no `/config` filesystem mount** — work only through
|
||||
`ha-cli`, and never try to read or write Home Assistant YAML files directly.
|
||||
|
||||
What is editable this way: automations, scripts, and scenes
|
||||
(`ha-cli get|post|delete config/automation/config/<id>` and the `script`/`scene` equivalents);
|
||||
service calls (`ha-cli call <domain.service> '<json>'`); state reads (`ha-cli states`); and,
|
||||
over WebSocket, helpers, dashboards, and area/label/floor/entity registries
|
||||
(`ha-cli ws '{"type":"..."}'`). Run `ha-cli --help` for the full reference. Raw YAML
|
||||
(`configuration.yaml`, `secrets.yaml`) is intentionally unreachable — if a change needs it,
|
||||
say so instead of working around it.
|
||||
|
||||
Rules: run `ha-cli config` first to confirm connectivity; **read the current object and show
|
||||
the user the intended change, then wait for confirmation** before any create/update/delete or
|
||||
any state-changing `call`; after writing, read the object back and reload if needed
|
||||
(e.g. `ha-cli call automation.reload`).
|
||||
<!-- END ha-api-helper (managed by claude_desktop addon) -->
|
||||
MD
|
||||
} >> "$CLAUDE_MD"
|
||||
fi
|
||||
elif [ -f "$CLAUDE_MD" ] && grep -qF "$HA_HELPER_GUIDE_BEGIN" "$CLAUDE_MD"; then
|
||||
bashio::log.info "Removing Home Assistant API helper guidance from CLAUDE.md"
|
||||
CLAUDE_MD="$CLAUDE_MD" python3 - <<'PY' || bashio::log.warning "Unable to remove Home Assistant API helper guidance automatically"
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(os.environ["CLAUDE_MD"])
|
||||
text = path.read_text(encoding="utf-8")
|
||||
pattern = re.compile(
|
||||
r"\n*<!-- BEGIN ha-api-helper \(managed by claude_desktop addon\) -->.*?"
|
||||
r"<!-- END ha-api-helper \(managed by claude_desktop addon\) -->\n?",
|
||||
re.DOTALL,
|
||||
)
|
||||
new = pattern.sub("", text)
|
||||
if new != text:
|
||||
path.write_text(new, encoding="utf-8")
|
||||
PY
|
||||
fi
|
||||
|
||||
if bashio::config.true 'install_rtk'; then
|
||||
if command -v rtk &> /dev/null; then
|
||||
if [ -f "$HOME/.claude/settings.json" ] && grep -q 'rtk hook claude' "$HOME/.claude/settings.json"; then
|
||||
bashio::log.info "rtk Claude Code hook already configured"
|
||||
else
|
||||
bashio::log.info "Configuring rtk Claude Code hook"
|
||||
RTK_NONINTERACTIVE=1 rtk init -g || bashio::log.warning "rtk global files configuration failed"
|
||||
python3 - <<'PY' || bashio::log.warning "Unable to configure rtk hook automatically"
|
||||
bashio::log.info "Configuring rtk Claude Code integration"
|
||||
run_as_runtime_user env RTK_NONINTERACTIVE=1 rtk init -g \
|
||||
|| bashio::log.warning "rtk global files configuration failed"
|
||||
python3 - <<'PY' || bashio::log.warning "Unable to configure rtk hook automatically"
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
path = Path.home() / ".claude" / "settings.json"
|
||||
try:
|
||||
data = json.loads(path.read_text()) if path.exists() else {}
|
||||
@@ -218,7 +552,6 @@ if not any("rtk hook claude" in json.dumps(entry) for entry in pre if isinstance
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
PY
|
||||
fi
|
||||
else
|
||||
bashio::log.warning "rtk is not available"
|
||||
fi
|
||||
@@ -290,7 +623,8 @@ if bashio::config.true 'install_caveman'; then
|
||||
bashio::log.info "caveman Claude Code plugin already configured"
|
||||
else
|
||||
bashio::log.info "Installing caveman Claude Code plugin"
|
||||
curl --connect-timeout 10 --max-time 60 -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash >/dev/null || bashio::log.warning "caveman install failed (offline?)"
|
||||
curl --connect-timeout 10 --max-time 60 -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash > /dev/null \
|
||||
|| bashio::log.warning "caveman install failed (offline?)"
|
||||
fi
|
||||
else
|
||||
bashio::log.info "Disabling caveman Claude Code plugin"
|
||||
@@ -298,9 +632,9 @@ else
|
||||
fi
|
||||
|
||||
# Startup configuration runs as root, while Claude Desktop runs as abc. Return managed
|
||||
# persistent files to the configured runtime UID/GID after all writes complete.
|
||||
# persistent files to the effective runtime UID/GID after all writes complete.
|
||||
for managed_path in "$HOME/.claude" "$HOME/.claude.json" "$HOME/.config/Claude"; do
|
||||
if [ -e "$managed_path" ]; then
|
||||
chown -R -- "${PUID}:${PGID}" "$managed_path" || bashio::log.warning "Unable to set ownership on $managed_path"
|
||||
chown -R -- "${RUNTIME_UID}:${RUNTIME_GID}" "$managed_path" || bashio::log.warning "Unable to set ownership on $managed_path"
|
||||
fi
|
||||
done
|
||||
|
||||
94
claude_desktop/rootfs/etc/cont-init.d/83-claude_permissions.sh
Executable file
94
claude_desktop/rootfs/etc/cont-init.d/83-claude_permissions.sh
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
# 20-folders.sh already remapped abc to the effective runtime identity (never root in bypass
|
||||
# mode), so follow abc instead of re-reading the raw PUID/PGID options here.
|
||||
RUNTIME_UID="$(id -u abc)"
|
||||
RUNTIME_GID="$(id -g abc)"
|
||||
PERMISSION_MODE="$(bashio::config 'permission_mode')"
|
||||
SETTINGS_PATH="$HOME/.claude/settings.json"
|
||||
STATE_PATH="$HOME/.claude/.addon-permission-mode.json"
|
||||
|
||||
case "$PERMISSION_MODE" in
|
||||
strict|auto|bypass) ;;
|
||||
*)
|
||||
bashio::log.warning "Unknown permission_mode '${PERMISSION_MODE}'; falling back to strict"
|
||||
PERMISSION_MODE="strict"
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "$(dirname "$SETTINGS_PATH")"
|
||||
PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" STATE_PATH="$STATE_PATH" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
mode = os.environ["PERMISSION_MODE"]
|
||||
settings_path = Path(os.environ["SETTINGS_PATH"])
|
||||
state_path = Path(os.environ["STATE_PATH"])
|
||||
|
||||
try:
|
||||
settings = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
||||
except (OSError, json.JSONDecodeError):
|
||||
if settings_path.exists():
|
||||
settings_path.rename(settings_path.with_suffix(settings_path.suffix + ".bak"))
|
||||
settings = {}
|
||||
if not isinstance(settings, dict):
|
||||
settings = {}
|
||||
|
||||
try:
|
||||
state = json.loads(state_path.read_text()) if state_path.exists() else None
|
||||
except (OSError, json.JSONDecodeError):
|
||||
state = None
|
||||
if not isinstance(state, dict):
|
||||
state = None
|
||||
|
||||
permissions = settings.get("permissions")
|
||||
if not isinstance(permissions, dict):
|
||||
permissions = {}
|
||||
|
||||
if mode == "strict":
|
||||
# Restore the value that existed before the add-on first managed this setting.
|
||||
if state is not None:
|
||||
if state.get("previous_exists"):
|
||||
permissions["defaultMode"] = state.get("previous_value")
|
||||
else:
|
||||
permissions.pop("defaultMode", None)
|
||||
state_path.unlink(missing_ok=True)
|
||||
else:
|
||||
if state is None:
|
||||
state = {
|
||||
"previous_exists": "defaultMode" in permissions,
|
||||
"previous_value": permissions.get("defaultMode"),
|
||||
}
|
||||
state_path.write_text(json.dumps(state, indent=2) + "\n")
|
||||
state_path.chmod(0o600)
|
||||
permissions["defaultMode"] = "auto" if mode == "auto" else "bypassPermissions"
|
||||
|
||||
if permissions:
|
||||
settings["permissions"] = permissions
|
||||
else:
|
||||
settings.pop("permissions", None)
|
||||
|
||||
settings_path.write_text(json.dumps(settings, indent=2) + "\n")
|
||||
settings_path.chmod(0o600)
|
||||
PY
|
||||
|
||||
case "$PERMISSION_MODE" in
|
||||
strict)
|
||||
bashio::log.info "Claude Code permission mode: strict (normal prompts)"
|
||||
;;
|
||||
auto)
|
||||
bashio::log.info "Claude Code permission mode: auto (safe actions approved automatically)"
|
||||
;;
|
||||
bypass)
|
||||
bashio::log.warning "Claude Code permission mode: bypass (permission checks disabled for mounted data and available tools)"
|
||||
;;
|
||||
esac
|
||||
|
||||
chown -- "${RUNTIME_UID}:${RUNTIME_GID}" "$SETTINGS_PATH" 2> /dev/null || true
|
||||
if [ -e "$STATE_PATH" ]; then
|
||||
chown -- "${RUNTIME_UID}:${RUNTIME_GID}" "$STATE_PATH" 2> /dev/null || true
|
||||
fi
|
||||
@@ -17,25 +17,54 @@ if ! command -v gh > /dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Everything below writes into the abc runtime user's HOME, so it must run AS abc. cont-init
|
||||
# runs as root with HOME already pointing at the persistent data location, so plain
|
||||
# `git config --global` recreated ~/.gitconfig owned by root:root on every start — and because
|
||||
# that file is rewritten each boot, 20-folders.sh's earlier recursive chown never stuck to it.
|
||||
# The user who actually runs git, gh and Claude was then unable to read its own committer
|
||||
# identity or the gh credential helper, so every commit failed with "Author identity unknown"
|
||||
# and authenticated pushes fell back to prompting. 20-folders.sh already remapped abc to the
|
||||
# effective runtime identity (never root in bypass mode), so follow abc rather than re-reading
|
||||
# the raw PUID/PGID options here.
|
||||
RUNTIME_UID="$(id -u abc)"
|
||||
RUNTIME_GID="$(id -g abc)"
|
||||
|
||||
run_as_runtime_user() {
|
||||
s6-setuidgid abc env HOME="$HOME" "$@"
|
||||
}
|
||||
|
||||
# Reclaim any root-owned copies left by an earlier add-on version before writing as abc:
|
||||
# these paths are not covered by 82-claude_tools.sh's ownership pass, and a root-owned
|
||||
# ~/.gitconfig would make the first `git config` below fail outright under `set -e`.
|
||||
|
||||
mkdir -p "$HOME/.config"
|
||||
chown -- "${RUNTIME_UID}:${RUNTIME_GID}" "$HOME/.config"
|
||||
|
||||
for managed_path in "$HOME/.gitconfig" "$HOME/.config/gh"; do
|
||||
if [ -e "$managed_path" ]; then
|
||||
chown -R -- "${RUNTIME_UID}:${RUNTIME_GID}" "$managed_path" || bashio::log.warning "Unable to set ownership on $managed_path"
|
||||
fi
|
||||
done
|
||||
|
||||
if bashio::config.has_value 'github_username'; then
|
||||
git config --global user.name "$(bashio::config 'github_username')"
|
||||
run_as_runtime_user git config --global user.name "$(bashio::config 'github_username')"
|
||||
fi
|
||||
|
||||
if bashio::config.has_value 'github_email'; then
|
||||
git config --global user.email "$(bashio::config 'github_email')"
|
||||
run_as_runtime_user git config --global user.email "$(bashio::config 'github_email')"
|
||||
fi
|
||||
|
||||
if bashio::config.has_value 'github_token'; then
|
||||
token="$(bashio::config 'github_token')"
|
||||
mkdir -p "$HOME/.config/gh"
|
||||
chmod 700 "$HOME/.config/gh"
|
||||
if env -u GH_TOKEN -u GITHUB_TOKEN gh auth status --hostname github.com > /dev/null 2>&1; then
|
||||
run_as_runtime_user mkdir -p "$HOME/.config/gh"
|
||||
run_as_runtime_user chmod 700 "$HOME/.config/gh"
|
||||
if run_as_runtime_user env -u GH_TOKEN -u GITHUB_TOKEN gh auth status --hostname github.com > /dev/null 2>&1; then
|
||||
bashio::log.info "GitHub CLI already authenticated for github.com"
|
||||
else
|
||||
bashio::log.info "Configuring GitHub CLI authentication for github.com"
|
||||
printf '%s\n' "$token" | env -u GH_TOKEN -u GITHUB_TOKEN gh auth login --hostname github.com --with-token || bashio::log.warning "GitHub CLI authentication failed"
|
||||
printf '%s\n' "$token" | run_as_runtime_user env -u GH_TOKEN -u GITHUB_TOKEN gh auth login --hostname github.com --with-token || bashio::log.warning "GitHub CLI authentication failed"
|
||||
fi
|
||||
env -u GH_TOKEN -u GITHUB_TOKEN gh auth setup-git --hostname github.com || bashio::log.warning "GitHub CLI git credential setup failed"
|
||||
run_as_runtime_user env -u GH_TOKEN -u GITHUB_TOKEN gh auth setup-git --hostname github.com || bashio::log.warning "GitHub CLI git credential setup failed"
|
||||
else
|
||||
bashio::log.info "GitHub CLI available. Set github_token to authenticate gh and git operations."
|
||||
fi
|
||||
|
||||
17
claude_desktop/rootfs/etc/cont-init.d/84-claude_runtime_ownership.sh
Executable file
17
claude_desktop/rootfs/etc/cont-init.d/84-claude_runtime_ownership.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
|
||||
# Earlier configuration scripts intentionally run as root. 20-folders.sh remapped abc to the
|
||||
# effective runtime identity (never root in bypass mode, where Claude Code refuses to run as
|
||||
# root). Reconcile ownership with that identity after all Claude configuration writes are
|
||||
# complete, as a safety net in case any intermediate step re-owned a managed path.
|
||||
RUNTIME_UID="$(id -u abc)"
|
||||
RUNTIME_GID="$(id -g abc)"
|
||||
|
||||
for managed_path in "$HOME/.claude" "$HOME/.claude.json" "$HOME/.config/Claude"; do
|
||||
if [ -e "$managed_path" ]; then
|
||||
chown -R -- "${RUNTIME_UID}:${RUNTIME_GID}" "$managed_path" \
|
||||
|| bashio::log.warning "Unable to set effective runtime ownership on $managed_path"
|
||||
fi
|
||||
done
|
||||
@@ -4,34 +4,105 @@ set -e
|
||||
|
||||
NGINX_CONFIG=/etc/nginx/sites-available/ingress.conf
|
||||
SUBFOLDER="$(bashio::addon.ingress_entry)"
|
||||
INGRESS_PORT="$(bashio::addon.ingress_port)"
|
||||
DOWNLOADS_PATH="${HOME:-/config}"
|
||||
|
||||
# Ensure subfolder ends with a trailing slash (except for root)
|
||||
# Home Assistant normally strips the ingress prefix before forwarding to the add-on,
|
||||
# but keep the normalized value available for diagnostics and future-safe logging.
|
||||
if [[ -n "${SUBFOLDER}" && "${SUBFOLDER}" != "/" ]]; then
|
||||
[[ "${SUBFOLDER}" == */ ]] || SUBFOLDER="${SUBFOLDER}/"
|
||||
else
|
||||
SUBFOLDER="/"
|
||||
fi
|
||||
|
||||
cp /defaults/default.conf "${NGINX_CONFIG}"
|
||||
# Claude Desktop exposes only 3001/tcp in config.yaml. Older Supervisor/bashio
|
||||
# combinations can return an empty ingress_port when it is not explicit, which would
|
||||
# make nginx write an invalid `listen` directive. Fall back to the declared port.
|
||||
if [[ -z "${INGRESS_PORT}" ]]; then
|
||||
INGRESS_PORT="3001"
|
||||
fi
|
||||
|
||||
# Keep only the first (non-SSL) server block
|
||||
awk -v n=2 '/^[[:space:]]*server[[:space:]]*\{/{n--} n>0' "${NGINX_CONFIG}" > tmpfile
|
||||
mv tmpfile "${NGINX_CONFIG}"
|
||||
DOWNLOADS_PATH="${DOWNLOADS_PATH%/}"
|
||||
|
||||
# Disable IPv6 listeners for ingress proxying
|
||||
sed -i '/listen \[::\]/d' "${NGINX_CONFIG}"
|
||||
cat > "${NGINX_CONFIG}" <<EOF
|
||||
server {
|
||||
listen ${INGRESS_PORT} default_server;
|
||||
client_max_body_size 10M;
|
||||
|
||||
# Adapt ports and upstream paths for Home Assistant ingress
|
||||
sed -i "s|3000|$(bashio::addon.ingress_port)|g" "${NGINX_CONFIG}"
|
||||
sed -i "s|SUBFOLDER|/|g" "${NGINX_CONFIG}"
|
||||
sed -i "s|CWS|8082|g" "${NGINX_CONFIG}"
|
||||
sed -i "s|REPLACE_HOME|${HOME:-/root}|g" "${NGINX_CONFIG}"
|
||||
sed -i "s|REPLACE_DOWNLOADS_PATH|${HOME:-/config}|g" "${NGINX_CONFIG}"
|
||||
sed -i '/proxy_buffering/a proxy_set_header Accept-Encoding "";' "${NGINX_CONFIG}"
|
||||
sed -i '/proxy_buffering/a sub_filter_once off;' "${NGINX_CONFIG}"
|
||||
sed -i '/proxy_buffering/a sub_filter_types *;' "${NGINX_CONFIG}"
|
||||
sed -i '/proxy_buffering/a sub_filter "vnc/index.html?autoconnect" "vnc/index.html?path=%%path%%/websockify?autoconnect";' "${NGINX_CONFIG}"
|
||||
sed -i "s|%%path%%|${SUBFOLDER:1}|g" "${NGINX_CONFIG}"
|
||||
location / {
|
||||
alias /usr/share/selkies/web/;
|
||||
index index.html index.htm;
|
||||
try_files \$uri \$uri/ /index.html;
|
||||
}
|
||||
|
||||
location /devmode {
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_http_version 1.1;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
proxy_pass http://127.0.0.1:5173;
|
||||
}
|
||||
|
||||
# Current Selkies WebSocket mode connects to <base>/api/websockets.
|
||||
# The older linuxserver default.conf only proxies /websocket, leaving the
|
||||
# dashboard loaded but stuck on "waiting for stream" under Home Assistant ingress.
|
||||
location /api/ {
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_http_version 1.1;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
proxy_pass http://127.0.0.1:8082;
|
||||
}
|
||||
|
||||
# Keep compatibility with older Selkies/noVNC clients and linuxserver templates.
|
||||
location /websocket {
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_http_version 1.1;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
proxy_pass http://127.0.0.1:8082;
|
||||
}
|
||||
|
||||
location /files {
|
||||
fancyindex on;
|
||||
fancyindex_footer /nginx/footer.html;
|
||||
fancyindex_header /nginx/header.html;
|
||||
alias ${DOWNLOADS_PATH}/;
|
||||
if (-f \$request_filename) {
|
||||
add_header Content-Disposition "attachment";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
}
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/selkies/web/;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Avoid content encoding on proxied responses to keep Selkies happy (handled by proxy_set_header Accept-Encoding insertion above)
|
||||
cp "${NGINX_CONFIG}" /etc/nginx/sites-enabled
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# Headroom optimization proxy — local backend for Claude Desktop MCP and Claude Code.
|
||||
declare port=8787
|
||||
# Bind all interfaces so the dashboard is reachable on the mapped host port
|
||||
# (http://<ha-ip>:8787/dashboard). Local consumers keep using 127.0.0.1.
|
||||
declare host=0.0.0.0
|
||||
declare host=127.0.0.1
|
||||
|
||||
if bashio::config.true 'install_headroom' && command -v headroom >/dev/null 2>&1; then
|
||||
# The dashboard is unauthenticated. Keep it container-local by default and bind all
|
||||
# interfaces only when the user explicitly opts in and maps port 8787.
|
||||
if bashio::config.true 'expose_headroom_dashboard'; then
|
||||
host=0.0.0.0
|
||||
fi
|
||||
|
||||
if bashio::config.true 'install_headroom' && command -v headroom > /dev/null 2>&1; then
|
||||
# Kompress (the ONNX compression engine) needs its model in the local HF cache: the
|
||||
# proxy's startup preload is deliberately cache-only, and the default HF cache lands
|
||||
# under ~/.cache, which the add-on points at tmpfs (/tmp/cache) — wiped on every
|
||||
# restart. Without a warm persistent cache the proxy ran forever in "deferred" mode
|
||||
# and recorded zero compression savings. Point the cache at persistent storage;
|
||||
# nothing else is needed here — the proxy's own request path already downloads a
|
||||
# missing model in the background on first use (ensure_background_load) and passes
|
||||
# requests through uncompressed until it lands, so this self-heals within a couple of
|
||||
# requests on the first boot and loads instantly (eager preload) on every boot after.
|
||||
# A synchronous pre-warm was tried here and removed: it blocked the port bind for up
|
||||
# to the download's duration, which left the settings-managed ANTHROPIC_BASE_URL
|
||||
# (see 82-claude_tools.sh) pointing at a proxy that wasn't listening yet.
|
||||
export HF_HOME="${HOME}/.headroom/hf"
|
||||
mkdir -p "$HF_HOME"
|
||||
chown abc:abc "$HF_HOME" 2> /dev/null || true
|
||||
bashio::log.info "svc-headroom: starting local Headroom proxy on ${host}:${port}"
|
||||
exec s6-setuidgid abc headroom proxy --host "${host}" --port "${port}" --code-aware
|
||||
fi
|
||||
|
||||
53
claude_desktop/rootfs/usr/local/bin/claude
Normal file
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,34 +1,44 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# Hourly rtk + headroom token-savings snapshot for the add-on log.
|
||||
# Invoked by cron (see /defaults/crontabs/root); its stdout is redirected to /proc/1/fd/1,
|
||||
# so the report appears in the add-on log. Doubles as a heartbeat: if the numbers stop
|
||||
# growing, the corresponding tool has stopped working.
|
||||
# with-contenv supplies HOME from the s6 envdir, so this honors a custom `data_location`
|
||||
# (see 20-folders.sh) instead of hardcoding /data/data; it also makes bashio::config
|
||||
# available for the install_headroom gate below.
|
||||
export NO_COLOR=1 # keep the add-on log free of ANSI color codes
|
||||
# Hourly RTK + Headroom + TokenSave token-savings snapshot for the add-on log.
|
||||
# Invoked by cron (see /defaults/crontabs/root); stdout is redirected to /proc/1/fd/1.
|
||||
# Each tool is reported independently so enabling Headroom cannot hide RTK or TokenSave data.
|
||||
# with-contenv supplies the configured persistent HOME.
|
||||
export NO_COLOR=1
|
||||
export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
|
||||
|
||||
have_rtk=false; command -v rtk >/dev/null 2>&1 && have_rtk=true
|
||||
have_headroom=false; command -v headroom >/dev/null 2>&1 && have_headroom=true
|
||||
|
||||
# headroom is pip-installed unconditionally at build time, so its binary is on PATH even
|
||||
# when install_headroom is off — gate on the same config svc-headroom checks, and only
|
||||
# fall back to have_headroom as a secondary availability guard.
|
||||
headroom_enabled=false
|
||||
if bashio::config.true 'install_headroom' && $have_headroom; then
|
||||
headroom_enabled=true
|
||||
if ! bashio::config.true 'enable_tools_health_report'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Nothing to report if neither tool is active — stay quiet.
|
||||
if ! $have_rtk && ! $headroom_enabled; then exit 0; fi
|
||||
rtk_enabled=false
|
||||
headroom_enabled=false
|
||||
tokensave_enabled=false
|
||||
|
||||
echo "===== claude gains report $(date '+%Y-%m-%d %H:%M:%S') ====="
|
||||
if bashio::config.true 'install_rtk' && command -v rtk > /dev/null 2>&1; then
|
||||
rtk_enabled=true
|
||||
fi
|
||||
if bashio::config.true 'install_headroom' && command -v headroom > /dev/null 2>&1; then
|
||||
headroom_enabled=true
|
||||
fi
|
||||
if bashio::config.true 'install_tokensave' && command -v tokensave > /dev/null 2>&1; then
|
||||
tokensave_enabled=true
|
||||
fi
|
||||
|
||||
if ! $rtk_enabled && ! $headroom_enabled && ! $tokensave_enabled; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "===== claude tools report $(date '+%Y-%m-%d %H:%M:%S') ====="
|
||||
if $headroom_enabled; then
|
||||
echo "--- headroom savings ---"
|
||||
headroom savings 2>&1 || echo "[warn] headroom savings failed"
|
||||
elif $have_rtk; then
|
||||
fi
|
||||
if $rtk_enabled; then
|
||||
echo "--- rtk gain ---"
|
||||
rtk gain 2>&1 || echo "[warn] rtk gain failed"
|
||||
fi
|
||||
echo "===== end gains report ====="
|
||||
if $tokensave_enabled; then
|
||||
echo "--- tokensave gain ---"
|
||||
tokensave gain --all --range 30d 2>&1 || echo "[warn] tokensave gain failed"
|
||||
fi
|
||||
echo "===== end claude tools report ====="
|
||||
|
||||
176
claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh
Executable file
176
claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh
Executable file
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# Diagnose installation, registration, routing, indexing, permissions, and recorded savings without
|
||||
# printing MCP environment values (which may contain the Home Assistant access token).
|
||||
# shellcheck shell=bash
|
||||
set +e
|
||||
set -o pipefail
|
||||
export NO_COLOR=1
|
||||
export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
|
||||
|
||||
section() {
|
||||
printf '\n=== %s ===\n' "$1"
|
||||
}
|
||||
|
||||
section "Installed binaries"
|
||||
for tool in claude claude-desktop headroom rtk tokensave git gh rg jq shellcheck yamllint hadolint actionlint; do
|
||||
resolved="$(command -v "$tool" 2> /dev/null || true)"
|
||||
if [ -n "$resolved" ]; then
|
||||
printf '%-16s %s\n' "$tool" "$resolved"
|
||||
else
|
||||
printf '%-16s %s\n' "$tool" "MISSING"
|
||||
fi
|
||||
done
|
||||
|
||||
section "Configured switches"
|
||||
for option in permission_mode install_headroom headroom_wrap_claude_code expose_headroom_dashboard install_rtk install_tokensave install_caveman enable_tools_health_report; do
|
||||
printf '%-30s %s\n' "$option" "$(bashio::config "$option")"
|
||||
done
|
||||
|
||||
section "Runtime identity"
|
||||
printf '%-30s %s\n' "configured PUID:PGID" "$(bashio::config 'PUID'):$(bashio::config 'PGID')"
|
||||
printf '%-30s %s\n' "effective abc UID:GID" "$(id -u abc):$(id -g abc)"
|
||||
printf '%-30s %s\n' "current process UID:GID" "$(id -u):$(id -g)"
|
||||
if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
|
||||
if [ "$(id -u abc)" -eq 0 ]; then
|
||||
echo "bypass runtime: ERROR - Claude Code will reject bypass permissions while abc is root"
|
||||
else
|
||||
echo "bypass runtime: OK - Claude Desktop and Cowork run as a non-root UID"
|
||||
fi
|
||||
fi
|
||||
|
||||
section "Claude Code permission state"
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
path = Path.home() / ".claude/settings.json"
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except FileNotFoundError:
|
||||
print("settings: MISSING")
|
||||
except Exception as exc:
|
||||
print(f"settings: INVALID: {exc}")
|
||||
else:
|
||||
permissions = data.get("permissions", {})
|
||||
if isinstance(permissions, dict):
|
||||
print(f"permissions.defaultMode: {permissions.get('defaultMode', '<upstream default>')}")
|
||||
else:
|
||||
print("permissions: INVALID")
|
||||
print(f"managed-state marker: {(Path.home() / '.claude/.addon-permission-mode.json').exists()}")
|
||||
PY
|
||||
|
||||
section "MCP registrations (environment values redacted)"
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
paths = [
|
||||
Path.home() / ".claude.json",
|
||||
Path.home() / ".config/Claude/claude_desktop_config.json",
|
||||
]
|
||||
for path in paths:
|
||||
print(path)
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except FileNotFoundError:
|
||||
print(" MISSING")
|
||||
continue
|
||||
except Exception as exc:
|
||||
print(f" INVALID: {exc}")
|
||||
continue
|
||||
servers = data.get("mcpServers", {})
|
||||
if not isinstance(servers, dict) or not servers:
|
||||
print(" no MCP servers")
|
||||
continue
|
||||
for name, spec in sorted(servers.items()):
|
||||
if not isinstance(spec, dict):
|
||||
print(f" {name}: invalid entry")
|
||||
continue
|
||||
command = spec.get("command", "?")
|
||||
args = spec.get("args", [])
|
||||
server_type = spec.get("type", "")
|
||||
suffix = f" type={server_type}" if server_type else ""
|
||||
print(f" {name}: {command} {args}{suffix}")
|
||||
if spec.get("env"):
|
||||
print(" env: <redacted>")
|
||||
PY
|
||||
|
||||
section "Claude Code hooks"
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
path = Path.home() / ".claude/settings.json"
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except FileNotFoundError:
|
||||
print("MISSING")
|
||||
raise SystemExit(0)
|
||||
except Exception as exc:
|
||||
print(f"INVALID: {exc}")
|
||||
raise SystemExit(0)
|
||||
|
||||
hooks = data.get("hooks", {})
|
||||
if not isinstance(hooks, dict) or not hooks:
|
||||
print("no hooks")
|
||||
raise SystemExit(0)
|
||||
for event, entries in hooks.items():
|
||||
print(event)
|
||||
if not isinstance(entries, list):
|
||||
print(" invalid entries")
|
||||
continue
|
||||
for entry in entries:
|
||||
matcher = entry.get("matcher", "*") if isinstance(entry, dict) else "?"
|
||||
commands = entry.get("hooks", []) if isinstance(entry, dict) else []
|
||||
rendered = []
|
||||
for command in commands if isinstance(commands, list) else []:
|
||||
if isinstance(command, dict):
|
||||
rendered.append(" ".join([str(command.get("command", "?")), *map(str, command.get("args", []))]))
|
||||
print(f" matcher={matcher}: {', '.join(rendered) or 'no command'}")
|
||||
PY
|
||||
|
||||
section "Headroom"
|
||||
if bashio::config.true 'install_headroom'; then
|
||||
curl -fsS --max-time 2 http://127.0.0.1:8787/health && echo || echo "proxy health: FAILED"
|
||||
headroom mcp status || true
|
||||
headroom savings || true
|
||||
else
|
||||
echo "disabled"
|
||||
fi
|
||||
|
||||
section "RTK"
|
||||
if bashio::config.true 'install_rtk'; then
|
||||
rtk gain || true
|
||||
else
|
||||
echo "disabled"
|
||||
fi
|
||||
|
||||
section "TokenSave"
|
||||
if bashio::config.true 'install_tokensave'; then
|
||||
tokensave doctor --agent claude || true
|
||||
tokensave gain --all --range 30d || true
|
||||
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
|
||||
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
|
||||
continue
|
||||
fi
|
||||
repo_root="$(s6-setuidgid abc env HOME="$HOME" git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
|
||||
if [ -z "$repo_root" ]; then
|
||||
echo "${configured_path}: not a Git repository"
|
||||
elif [ -f "$repo_root/.tokensave/tokensave.db" ]; then
|
||||
s6-setuidgid abc env HOME="$HOME" tokensave status "$repo_root" --short || true
|
||||
else
|
||||
echo "${repo_root}: NOT INITIALIZED"
|
||||
fi
|
||||
done < <(bashio::config 'tokensave_project_paths')
|
||||
else
|
||||
echo "disabled"
|
||||
fi
|
||||
|
||||
section "Claude routing"
|
||||
printf 'PATH claude: %s\n' "$(command -v claude 2> /dev/null || true)"
|
||||
printf 'real claude: %s\n' "$([ -x /usr/bin/claude ] && echo /usr/bin/claude || echo MISSING)"
|
||||
if bashio::config.true 'headroom_wrap_claude_code'; then
|
||||
echo "PATH-based Claude Code launches are configured for Headroom wrapping."
|
||||
else
|
||||
echo "Claude Code Headroom wrapping is disabled; Headroom remains available through MCP."
|
||||
fi
|
||||
236
claude_desktop/rootfs/usr/local/bin/ha-cli
Executable file
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
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)
|
||||
@@ -1,3 +1,5 @@
|
||||
## 1.4.0-2 (16-07-2026)
|
||||
- Minor bugs fixed
|
||||
|
||||
## 1.4.0 (2026-06-20)
|
||||
- Update to latest version from gtsteffaniak/filebrowser (changelog : https://github.com/gtsteffaniak/filebrowser/releases)
|
||||
|
||||
@@ -115,4 +115,5 @@ LABEL \
|
||||
# 6 Healthcheck #
|
||||
#################
|
||||
|
||||
# Upstream
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||
CMD curl -f http://localhost:3001/health || exit 1
|
||||
|
||||
@@ -114,4 +114,4 @@ schema:
|
||||
slug: filebrowser_quantum
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "1.4.0"
|
||||
version: "1.4.0-2"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
## 1.27 (15-07-2026)
|
||||
- Fix the Gitea add-on HEALTHCHECK to work correctly when SSL is enabled, so Home Assistant can accurately report the add-on's health status regardless of whether the instance uses HTTP or HTTPS.
|
||||
|
||||
## 1.26.4 (2026-06-23)
|
||||
- Update to latest version from go-gitea/gitea (changelog : https://github.com/go-gitea/gitea/releases)
|
||||
|
||||
@@ -133,4 +133,4 @@ HEALTHCHECK \
|
||||
--retries=5 \
|
||||
--start-period=30s \
|
||||
--timeout=25s \
|
||||
CMD curl -A "HealthCheck: Docker/1.0" -s -f "http://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" &>/dev/null || exit 1
|
||||
CMD curl -A "HealthCheck: Docker/1.0" -s -f -k --http1.1 "$(cat /run/health_protocol 2>/dev/null || echo http)://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" &>/dev/null || exit 1
|
||||
|
||||
@@ -97,5 +97,5 @@ schema:
|
||||
slug: gitea
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/gitea
|
||||
version: "1.26.4"
|
||||
version: "1.27"
|
||||
webui: "[PROTO:ssl]://[HOST]:[PORT:3000]"
|
||||
|
||||
@@ -55,6 +55,7 @@ for file in /config/app.ini /etc/templates/app.ini; do
|
||||
PROTOCOL=http
|
||||
sed -i "/server/a PROTOCOL=http" "$file"
|
||||
fi
|
||||
echo -n "${PROTOCOL}" > /run/health_protocol
|
||||
|
||||
##################
|
||||
# ADAPT ROOT_URL #
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
## ⚠ Open Issue : [🐛 [qBittorrent] Cant update to 5.2.3 (opened 2026-07-09)](https://github.com/alexbelgium/hassio-addons/issues/2836) by [@tschoehuijs](https://github.com/tschoehuijs)
|
||||
## ⚠ Open Request : [✨ [REQUEST] transmission, document where torrent files are stored (opened 2026-07-14)](https://github.com/alexbelgium/hassio-addons/issues/2852) by [@bilogic](https://github.com/bilogic)
|
||||
# Hass.io Add-ons: Tor with bridges
|
||||
|
||||
[![Donate][donation-badge]](https://www.buymeacoffee.com/alexbelgium)
|
||||
|
||||
Reference in New Issue
Block a user