Compare commits

...

28 Commits

Author SHA1 Message Date
Alexandre
26c26e3668 Update ownership to user 'abc' in 20-folders.sh
Change ownership of specified directories to user 'abc'.
2026-07-15 17:29:54 +02:00
github-actions
64c7f2dd5b GitHub bot: changelog [nobuild] 2026-07-15 15:24:28 +00:00
Alexandre
259517289a build 2026-07-15 17:12:15 +02:00
github-actions
b26da2e161 GitHub bot: changelog [nobuild] 2026-07-15 15:08:23 +00:00
Alexandre
9ec6c44f03 Update config.yaml 2026-07-15 17:06:02 +02:00
Alexandre
ff43c4eeba Update CHANGELOG.md 2026-07-15 17:05:51 +02:00
Alexandre
a1e68ee807 Merge pull request #2867 from crazyrokr/feature/gitea-ssl-healthcheck
Fix Gitea HEALTHCHECK in case of SSL setup
2026-07-15 17:05:04 +02:00
Maksim Kashapov
2a1412d957 Fix Gitea HEALTHCHECK in case of SSL setup 2026-07-15 16:10:54 +02:00
github-actions
b328ae242f Github bot : issues linked to readme 2026-07-15 14:01:27 +00:00
Alexandre
9c0521da49 Merge pull request #2865 from alexbelgium/fix/claude-wrapper-headroom-path
fix(claude_desktop): headroom wrapper path + TokenSave startup corruption hardening
2026-07-15 14:58:12 +02:00
alexbelgium
7d2c6eb9b2 fix(claude_desktop): self-heal TokenSave graph against corruption on startup
The startup indexer chose init vs sync purely on whether
.tokensave/tokensave.db existed, so an interrupted init or a hard
add-on stop mid-write could leave a partial or malformed SQLite graph
that every subsequent boot then ran `sync` against, failing (and
staying broken) forever.

Prepare each configured repo defensively instead:
- serialize the operation under a startup-scoped flock so an
  overlapping restart or a mid-boot git post-commit/checkout sync hook
  can't write the same DB concurrently;
- refresh an existing index with a retried incremental sync, since
  SQLITE_BUSY from lock contention is transient, not corruption;
- quarantine a genuinely unreadable index (sync still failing after
  retries) or a half-written one (an interrupted init, detected via a
  sentinel file) to .tokensave/corrupt-<timestamp>/ and rebuild from
  scratch, so the graph self-heals rather than propagating corruption.

All file operations run as the abc runtime user because the repo
.tokensave directory is outside this script's final ownership pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 14:42:41 +02:00
alexbelgium
f990177df8 fix(claude_desktop): resolve headroom binary via PATH in claude wrapper
The /usr/local/bin/claude wrapper hardcoded HEADROOM_BIN as
/usr/local/bin/headroom, but the binary is installed at
/usr/bin/headroom (symlink to /lsiopy/bin/headroom). The -x check
therefore always failed and terminal Claude Code sessions never
routed through the Headroom proxy at 127.0.0.1:8787.

Resolve the binary with "command -v headroom" instead; an empty
result still fails the -x check safely and falls back to launching
Claude Code directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 14:32:35 +02:00
Alexandre
005275315a Merge pull request #2863 from alexbelgium/feat/claude_desktop-ha-api-helper nobuild
feat(claude_desktop): add ha-cli Core-API helper for configuring Home Assistant
2026-07-15 14:25:21 +02:00
alexbelgium
0223cc3511 fix(claude_desktop): address ha-cli review findings
- Check ha_mcp_token before SUPERVISOR_TOKEN: this add-on always sets
  homeassistant_api, so the admin-equivalent Supervisor token was always
  present and silently shadowed a user's deliberately scoped-down
  ha_mcp_token, defeating the documented scoping path (Codex P1).
- Make ha-cli itself refuse to run when enable_ha_api_helper is false,
  instead of only removing the CLAUDE.md guidance text — disabling the
  option now actually disables the helper (Codex P2).
- Normalize HA_BASE_URL to include /api when the user omits it, so REST
  calls don't 404 (CodeRabbit).
- Read/write CLAUDE.md with explicit UTF-8 in the ha-api-helper removal
  block, matching the emoji/special characters Claude tends to write
  there (CodeRabbit).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 14:24:17 +02:00
alexbelgium
e5983f4718 feat(claude_desktop): add ha-cli Core-API helper for configuring Home Assistant
Ship a `ha-cli` command that lets Claude configure Home Assistant
(automations, scripts, scenes, helpers, dashboards, registries, service
calls) through the Home Assistant Core API instead of a /config filesystem
mount, so secrets.yaml and other add-ons' credentials stay out of reach.

It authenticates automatically with the add-on's SUPERVISOR_TOKEN via the
Supervisor Core-API proxy (no token setup), with optional HA_TOKEN /
ha_mcp_token overrides for a scoped Home Assistant user. A managed guidance
block in ~/.claude/CLAUDE.md tells Claude Code to use the helper and to
confirm before writes. Gated by the new enable_ha_api_helper option
(default on). Adds the websockets dependency for the WebSocket subcommand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 14:05:29 +02:00
Alexandre
587121cb1b Update config.yaml 2026-07-15 13:29:35 +02:00
Alexandre
29f2cfd198 Update 81-claude_update.sh 2026-07-15 13:29:07 +02:00
Alexandre
3329c4b40d nobuild 2026-07-15 13:17:03 +02:00
github-actions
9471dffcca GitHub bot: sanitize (spaces + LF endings) & chmod [nobuild] 2026-07-15 09:18:15 +00:00
Alexandre
215327e439 Merge pull request #2862 from alexbelgium/agent/claude-bypass-nonroot
Fix Claude bypass permissions under root PUID
2026-07-15 11:17:27 +02:00
Alexandre
8ecbfccdcd Document Claude bypass root fix 2026-07-15 11:05:49 +02:00
Alexandre
5a2efc4f9e Document non-root Claude bypass runtime 2026-07-15 11:04:27 +02:00
Alexandre
ef0aecbde6 Bump Claude Desktop add-on version 2026-07-15 11:02:55 +02:00
Alexandre
7a6ad72617 Diagnose Claude bypass runtime identity 2026-07-15 11:02:23 +02:00
Alexandre
b5985f230e Restore effective Claude runtime ownership 2026-07-15 11:01:22 +02:00
Alexandre
d48d1f4d8d Drop root for Claude bypass launches 2026-07-15 11:01:06 +02:00
Alexandre
6bf379bffc Use effective Claude runtime ownership 2026-07-15 10:59:49 +02:00
Alexandre
5d577ad954 Run Claude bypass mode as non-root 2026-07-15 10:59:09 +02:00
16 changed files with 504 additions and 40 deletions

View File

@@ -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`.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View 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

View File

@@ -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`

View File

@@ -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

View 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:]))

View File

@@ -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)

View File

@@ -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

View File

@@ -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]"

View File

@@ -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 #

View File

@@ -1,4 +1,5 @@
## &#9888; 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)
## &#9888; 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)