mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-10-13 03:31:35 +02:00
Compare commits
28 Commits
85dab2ae6e
...
26c26e3668
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -1,3 +1,22 @@
|
||||
## 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`.
|
||||
|
||||
@@ -74,7 +74,7 @@ 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
|
||||
@@ -146,7 +146,7 @@ RUN /usr/local/bin/rtk --version && /usr/local/bin/tokensave --version
|
||||
# installer used for the additional_pip option).
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
pip3 install --break-system-packages "headroom-ai[proxy,code,mcp]" mcp-proxy uv && \
|
||||
pip3 install --break-system-packages "headroom-ai[proxy,code,mcp]" mcp-proxy uv websockets && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /root/.cache
|
||||
|
||||
|
||||
@@ -35,6 +35,10 @@ PATH tools.
|
||||
`/usr/bin/claude` directly, the session remains functional and still has the
|
||||
shared permission mode and Headroom MCP tools, but transparent proxy
|
||||
compression cannot be injected.
|
||||
- When `permission_mode: bypass` is selected while `PUID` is `0`, the add-on
|
||||
automatically remaps the shared `abc` desktop account to an unused non-root
|
||||
UID before Selkies and Claude Desktop start. Claude Code refuses bypass mode
|
||||
under an effective root UID.
|
||||
- **gnome-keyring** provides the Secret Service backend Electron needs to
|
||||
persist sign-in and dispatch permission grants across restarts.
|
||||
|
||||
@@ -63,6 +67,8 @@ Git synchronization hooks. A repository is indexed only when it is listed in
|
||||
- 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`).
|
||||
- Baked-in `git`, GitHub CLI (`gh`), `ripgrep`, `jq`, `shellcheck`, `yamllint`,
|
||||
@@ -74,14 +80,15 @@ Git synchronization hooks. A repository is indexed only when it is listed in
|
||||
Assistant.
|
||||
- Independent hourly savings reports for Headroom, RTK, and TokenSave.
|
||||
- `claude-tools-doctor.sh` diagnostics for binaries, routing, hooks, MCP
|
||||
registrations, project indexes, proxy health, permissions, and gains.
|
||||
registrations, project indexes, proxy health, permissions, runtime identity,
|
||||
and gains.
|
||||
- Low-power defaults for GPU mapping, Selkies frame rate, and volatile caches.
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------ | ------- | ----------- |
|
||||
| `PUID` / `PGID` | `0` / `0` | Numeric user and group applied by the LinuxServer initialization. |
|
||||
| `PUID` / `PGID` | `0` / `0` | Numeric user and group applied by LinuxServer initialization. In bypass mode, a root `PUID` is automatically replaced at runtime by an unused non-root UID while the configured group is retained. |
|
||||
| `TZ` | | Optional timezone, for example `Europe/Brussels`. |
|
||||
| `KEYBOARD` | | Optional Selkies keyboard layout. |
|
||||
| `PASSWORD` | | Optional password for direct Selkies ports. |
|
||||
@@ -104,6 +111,7 @@ Git synchronization hooks. A repository is indexed only when it is listed in
|
||||
| `enable_ha_mcp` | `false` | Register Home Assistant's MCP server in Claude (requires `ha_mcp_token`). |
|
||||
| `ha_mcp_url` | `http://homeassistant:8123/api/mcp` | Streamable HTTP endpoint of Home Assistant's MCP Server integration. |
|
||||
| `ha_mcp_token` | | Home Assistant long-lived access token used by the MCP bridge. |
|
||||
| `enable_ha_api_helper` | `true` | Ship the `ha-cli` Core-API helper and add guidance so Claude can configure Home Assistant without a `/config` mount. |
|
||||
| `additional_apps` | | Comma-separated Debian apt packages to install at startup. |
|
||||
| `additional_pip` | | Comma-separated pip packages installed at startup (via `uv`). |
|
||||
| `data_location` | `/data/data` | Persistent home directory for Claude and tooling. |
|
||||
@@ -122,9 +130,21 @@ permission_mode: auto
|
||||
`bypassPermissions` in the shared settings and
|
||||
`--dangerously-skip-permissions` for wrapper-launched sessions.
|
||||
|
||||
Claude Code does not permit bypass mode when its effective UID is `0`. If the
|
||||
add-on is configured with `PUID: 0`, selecting `bypass` remaps only the shared
|
||||
`abc` runtime account to an available non-root UID (preferring `1000`, then
|
||||
`911`) before storage ownership and Desktop startup. Its configured primary
|
||||
GID is retained, so group-based access to mounted Home Assistant paths remains
|
||||
available. Strict and auto modes keep the configured identity unchanged.
|
||||
|
||||
A root shell invoking `/usr/local/bin/claude` in bypass mode is also dropped to
|
||||
the remapped `abc` account. Directly invoking `/usr/bin/claude` as root still
|
||||
bypasses the add-on wrapper and will be rejected by Claude Code.
|
||||
|
||||
`bypass` gives Claude broad authority over all mounted writable data and every
|
||||
command or credential available inside the add-on. Enable it only in a trusted
|
||||
installation with trusted repositories and mounts.
|
||||
installation with trusted repositories and mounts. Mounted paths must remain
|
||||
accessible to the effective non-root UID or its retained group.
|
||||
|
||||
### TokenSave project example
|
||||
|
||||
@@ -171,10 +191,11 @@ Run the following inside the add-on through a custom script or container console
|
||||
claude-tools-doctor.sh
|
||||
```
|
||||
|
||||
The report checks the tool binaries, configuration switches, 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 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:
|
||||
|
||||
@@ -196,6 +217,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
|
||||
|
||||
@@ -13,8 +13,8 @@ environment:
|
||||
AUTO_GPU: "1"
|
||||
FM_HOME: /data/data
|
||||
HOME: /data/data
|
||||
PGID: "0"
|
||||
PUID: "0"
|
||||
PGID: "1000"
|
||||
PUID: "1000"
|
||||
SELKIES_FRAMERATE: "30"
|
||||
START_DOCKER: "false"
|
||||
TITLE: Claude Desktop
|
||||
@@ -35,8 +35,6 @@ options:
|
||||
env_vars: []
|
||||
DNS_server: 8.8.8.8
|
||||
data_location: /data/data
|
||||
PGID: 0
|
||||
PUID: 0
|
||||
additional_apps: ""
|
||||
additional_pip: ""
|
||||
auto_update: true
|
||||
@@ -44,6 +42,7 @@ 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: ""
|
||||
enable_tools_health_report: true
|
||||
@@ -76,16 +75,19 @@ schema:
|
||||
DRINODE: list(/dev/dri/card0|/dev/dri/card1|/dev/dri/card2|/dev/dri/renderD128|/dev/dri/renderD129|)?
|
||||
KEYBOARD: list(da-dk-qwerty|de-de-qwertz|en-gb-qwerty|en-us-qwerty|es-es-qwerty|fr-ch-qwertz|fr-fr-azerty|it-it-qwerty|ja-jp-qwerty|pt-br-qwerty|sv-se-qwerty|tr-tr-qwerty)?
|
||||
PASSWORD: str?
|
||||
PGID: int
|
||||
PUID: int
|
||||
TZ: match([A-Z][a-z]*./[A-Z][a-z]*.)?
|
||||
additional_apps: str?
|
||||
additional_pip: str?
|
||||
auto_update: bool?
|
||||
cifsdomain: str?
|
||||
cifspassword: str?
|
||||
cifsusername: str?
|
||||
localdisks: str?
|
||||
networkdisks: str?
|
||||
github_email: str?
|
||||
enable_ha_mcp: bool?
|
||||
ha_mcp_url: str?
|
||||
ha_mcp_token: password?
|
||||
enable_ha_api_helper: bool?
|
||||
github_token: password?
|
||||
github_username: str?
|
||||
enable_tools_health_report: bool
|
||||
@@ -103,5 +105,5 @@ slug: claude_desktop
|
||||
tmpfs: true
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "1.20"
|
||||
version: "1.25"
|
||||
video: true
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
# shellcheck disable=SC2046
|
||||
set -e
|
||||
|
||||
# Define user
|
||||
PUID=$(bashio::config "PUID")
|
||||
PGID=$(bashio::config "PGID")
|
||||
# Use the effective shared desktop user identity. In bypass mode an earlier init script may
|
||||
# remap abc away from UID 0 because Claude Code rejects bypass permissions when run as root.
|
||||
PUID=1000
|
||||
PGID=1000
|
||||
|
||||
# Check data location
|
||||
LOCATION="$(bashio::config 'data_location')"
|
||||
@@ -81,7 +82,7 @@ fi
|
||||
ln -sfn /tmp/cache "$LOCATION/.cache"
|
||||
|
||||
bashio::log.info "Setting ownership to $PUID:$PGID"
|
||||
chown -R "$PUID":"$PGID" "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR"
|
||||
chown -R abc "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR" /data
|
||||
chmod -R 700 "$LOCATION"
|
||||
|
||||
# The base init-selkies-config script overrides XDG_RUNTIME_DIR to $HOME/.XDG, which lands
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -193,15 +193,64 @@ if $TOKENSAVE_ENABLED; then
|
||||
fi
|
||||
TOKENSAVE_REPOS_SEEN[$repo_root]=1
|
||||
|
||||
if [ -f "$repo_root/.tokensave/tokensave.db" ]; then
|
||||
bashio::log.info "Synchronizing TokenSave index: ${repo_root}"
|
||||
run_as_runtime_user tokensave sync "$repo_root" \
|
||||
|| bashio::log.warning "TokenSave sync failed for ${repo_root}"
|
||||
else
|
||||
bashio::log.info "Initializing TokenSave index: ${repo_root}"
|
||||
run_as_runtime_user tokensave init "$repo_root" \
|
||||
|| bashio::log.warning "TokenSave initialization failed for ${repo_root}"
|
||||
fi
|
||||
bashio::log.info "Preparing TokenSave index: ${repo_root}"
|
||||
# Prepare the per-repo semantic graph defensively so a hard add-on stop or storage
|
||||
# hiccup can never leave a broken index that fails every subsequent boot:
|
||||
# * a startup-scoped flock serializes against an overlapping restart (and any git
|
||||
# post-commit/checkout sync hook that fires mid-boot), so two writers never race;
|
||||
# * an existing index is refreshed with a cheap incremental `sync`, retried a few
|
||||
# times because SQLITE_BUSY under lock contention is transient, not corruption;
|
||||
# * only a genuinely unreadable/malformed index (sync still failing after retries)
|
||||
# or a half-written one from an interrupted `init` is quarantined and rebuilt,
|
||||
# so the graph self-heals instead of propagating corruption;
|
||||
# * `init` is bracketed by a sentinel file so an interrupted full build is detected
|
||||
# as incomplete on the next start and rebuilt rather than trusted.
|
||||
# All file operations run as the abc runtime user because the repo `.tokensave`
|
||||
# directory is not covered by this script's final ownership pass.
|
||||
# shellcheck disable=SC2016 # single-quoted on purpose: $1/$db/etc. expand in the abc shell
|
||||
run_as_runtime_user bash -c '
|
||||
set -o pipefail
|
||||
repo_root="$1"
|
||||
ts_dir="$repo_root/.tokensave"
|
||||
db="$ts_dir/tokensave.db"
|
||||
lock="$ts_dir/.startup.lock"
|
||||
initflag="$ts_dir/.init-incomplete"
|
||||
mkdir -p "$ts_dir"
|
||||
exec 9>"$lock"
|
||||
if ! flock -n 9; then
|
||||
echo "TokenSave: index busy for $repo_root; skipping startup sync" >&2
|
||||
exit 0
|
||||
fi
|
||||
quarantine() {
|
||||
stamp="$(date +%Y%m%d-%H%M%S)"
|
||||
bdir="$ts_dir/corrupt-$stamp"
|
||||
mkdir -p "$bdir"
|
||||
for f in "$db" "$db-wal" "$db-shm"; do
|
||||
[ -e "$f" ] && mv -f "$f" "$bdir/" 2>/dev/null || true
|
||||
done
|
||||
echo "TokenSave: quarantined suspect index to $bdir" >&2
|
||||
}
|
||||
if [ -f "$db" ] && [ ! -f "$initflag" ]; then
|
||||
attempt=1
|
||||
while :; do
|
||||
tokensave sync "$repo_root" && exit 0
|
||||
[ "$attempt" -ge 3 ] && break
|
||||
echo "TokenSave: sync attempt $attempt failed for $repo_root; retrying" >&2
|
||||
attempt=$((attempt + 1))
|
||||
sleep 2
|
||||
done
|
||||
echo "TokenSave: sync failed after retries for $repo_root; rebuilding index" >&2
|
||||
quarantine
|
||||
elif [ -f "$db" ]; then
|
||||
echo "TokenSave: previous init did not finish for $repo_root; rebuilding index" >&2
|
||||
quarantine
|
||||
fi
|
||||
: > "$initflag"
|
||||
tokensave init "$repo_root" && { rm -f "$initflag"; exit 0; }
|
||||
echo "TokenSave: init failed for $repo_root; will retry on next start" >&2
|
||||
exit 1
|
||||
' _ "$repo_root" \
|
||||
|| bashio::log.warning "TokenSave preparation failed for ${repo_root}"
|
||||
done < <(bashio::config.array 'tokensave_project_paths')
|
||||
fi
|
||||
|
||||
@@ -251,6 +300,61 @@ if new != text:
|
||||
PY
|
||||
fi
|
||||
|
||||
# Tell Claude Code that it can configure Home Assistant over the Core API via the shipped
|
||||
# `ha-cli` helper (no /config filesystem mount needed). Managed, idempotent block appended to
|
||||
# the user's global CLAUDE.md; removed when the helper is disabled. Mirrors the headroom block.
|
||||
HA_HELPER_GUIDE_BEGIN="<!-- BEGIN ha-api-helper (managed by claude_desktop addon) -->"
|
||||
if bashio::config.true 'enable_ha_api_helper'; then
|
||||
mkdir -p "$(dirname "$CLAUDE_MD")"
|
||||
if ! { [ -f "$CLAUDE_MD" ] && grep -qF "$HA_HELPER_GUIDE_BEGIN" "$CLAUDE_MD"; }; then
|
||||
bashio::log.info "Adding Home Assistant API helper guidance to CLAUDE.md"
|
||||
{
|
||||
[ -s "$CLAUDE_MD" ] && printf '\n'
|
||||
cat <<'MD'
|
||||
<!-- BEGIN ha-api-helper (managed by claude_desktop addon) -->
|
||||
## Configuring Home Assistant
|
||||
|
||||
You can configure this Home Assistant instance through its Core API using the `ha-cli`
|
||||
command (on `PATH`). It authenticates automatically with the add-on's `$SUPERVISOR_TOKEN`,
|
||||
so no token setup is needed. There is **no `/config` filesystem mount** — work only through
|
||||
`ha-cli`, and never try to read or write Home Assistant YAML files directly.
|
||||
|
||||
What is editable this way: automations, scripts, and scenes
|
||||
(`ha-cli get|post|delete config/automation/config/<id>` and the `script`/`scene` equivalents);
|
||||
service calls (`ha-cli call <domain.service> '<json>'`); state reads (`ha-cli states`); and,
|
||||
over WebSocket, helpers, dashboards, and area/label/floor/entity registries
|
||||
(`ha-cli ws '{"type":"..."}'`). Run `ha-cli --help` for the full reference. Raw YAML
|
||||
(`configuration.yaml`, `secrets.yaml`) is intentionally unreachable — if a change needs it,
|
||||
say so instead of working around it.
|
||||
|
||||
Rules: run `ha-cli config` first to confirm connectivity; **read the current object and show
|
||||
the user the intended change, then wait for confirmation** before any create/update/delete or
|
||||
any state-changing `call`; after writing, read the object back and reload if needed
|
||||
(e.g. `ha-cli call automation.reload`).
|
||||
<!-- END ha-api-helper (managed by claude_desktop addon) -->
|
||||
MD
|
||||
} >> "$CLAUDE_MD"
|
||||
fi
|
||||
elif [ -f "$CLAUDE_MD" ] && grep -qF "$HA_HELPER_GUIDE_BEGIN" "$CLAUDE_MD"; then
|
||||
bashio::log.info "Removing Home Assistant API helper guidance from CLAUDE.md"
|
||||
CLAUDE_MD="$CLAUDE_MD" python3 - <<'PY' || bashio::log.warning "Unable to remove Home Assistant API helper guidance automatically"
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(os.environ["CLAUDE_MD"])
|
||||
text = path.read_text(encoding="utf-8")
|
||||
pattern = re.compile(
|
||||
r"\n*<!-- BEGIN ha-api-helper \(managed by claude_desktop addon\) -->.*?"
|
||||
r"<!-- END ha-api-helper \(managed by claude_desktop addon\) -->\n?",
|
||||
re.DOTALL,
|
||||
)
|
||||
new = pattern.sub("", text)
|
||||
if new != text:
|
||||
path.write_text(new, encoding="utf-8")
|
||||
PY
|
||||
fi
|
||||
|
||||
if bashio::config.true 'install_rtk'; then
|
||||
if command -v rtk &> /dev/null; then
|
||||
bashio::log.info "Configuring rtk Claude Code integration"
|
||||
|
||||
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 and may use the configured PUID/PGID
|
||||
# values when returning files to the runtime user. In bypass mode PUID can still be configured as
|
||||
# 0 even though 19-claude_bypass_runtime.sh remapped abc to a non-root UID. Reconcile ownership
|
||||
# with the effective desktop identity after all Claude configuration writes are complete.
|
||||
RUNTIME_UID="$(id -u abc)"
|
||||
RUNTIME_GID="$(id -g abc)"
|
||||
|
||||
for managed_path in "$HOME/.claude" "$HOME/.claude.json" "$HOME/.config/Claude"; do
|
||||
if [ -e "$managed_path" ]; then
|
||||
chown -R -- "${RUNTIME_UID}:${RUNTIME_GID}" "$managed_path" \
|
||||
|| bashio::log.warning "Unable to set effective runtime ownership on $managed_path"
|
||||
fi
|
||||
done
|
||||
@@ -3,7 +3,7 @@
|
||||
set -o pipefail
|
||||
|
||||
REAL_CLAUDE="/usr/bin/claude"
|
||||
HEADROOM_BIN="/usr/local/bin/headroom"
|
||||
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=()
|
||||
@@ -27,6 +27,17 @@ if [ ! -x "$REAL_CLAUDE" ]; then
|
||||
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`
|
||||
|
||||
@@ -26,6 +26,18 @@ for option in permission_mode install_headroom headroom_wrap_claude_code expose_
|
||||
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
|
||||
|
||||
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:]))
|
||||
@@ -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