Compare commits

...

15 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
14 changed files with 415 additions and 66 deletions

View File

@@ -1,3 +1,14 @@
## 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`.

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

@@ -111,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. |
@@ -216,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

@@ -42,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
@@ -86,6 +87,7 @@ schema:
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.22"
version: "1.25"
video: true

View File

@@ -1,48 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
set -o pipefail
# Claude Code deliberately refuses bypass-permissions mode when its effective UID is 0.
# The add-on historically defaults PUID to 0, so switch the shared `abc` desktop user to
# an unused non-root UID before storage ownership and Selkies runtime directories are set up.
# Keep abc's configured primary group (commonly group 0) so existing group-based access to
# Home Assistant mounts is preserved. Strict and auto permission modes are unchanged.
if [ "$(bashio::config 'permission_mode')" != "bypass" ]; then
exit 0
fi
CURRENT_UID="$(id -u abc)"
if [ "$CURRENT_UID" -ne 0 ]; then
bashio::log.info "Claude bypass runtime already uses non-root UID ${CURRENT_UID}"
exit 0
fi
find_available_uid() {
local candidate owner
for candidate in 1000 911 $(seq 1001 1099); do
owner="$(getent passwd "$candidate" | cut -d: -f1 || true)"
if [ -z "$owner" ] || [ "$owner" = "abc" ]; then
printf '%s' "$candidate"
return 0
fi
done
return 1
}
TARGET_UID="$(find_available_uid || true)"
if [ -z "$TARGET_UID" ]; then
bashio::exit.nok "Claude bypass mode requires a non-root runtime user, but no free fallback UID was found"
fi
usermod --uid "$TARGET_UID" abc
if [ "$(id -u abc)" -eq 0 ]; then
bashio::exit.nok "Unable to switch the Claude Desktop runtime away from root for bypass mode"
fi
mkdir -p /run/s6/container_environment
printf '%s' "$TARGET_UID" > /run/s6/container_environment/CLAUDE_RUNTIME_UID
printf '%s' "$(id -g abc)" > /run/s6/container_environment/CLAUDE_RUNTIME_GID
bashio::log.warning "Claude bypass mode cannot run as root; remapped abc from UID 0 to UID ${TARGET_UID} (GID $(id -g abc))"

View File

@@ -5,8 +5,8 @@ set -e
# 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="$(id -u abc)"
PGID="$(id -g abc)"
PUID=1000
PGID=1000
# Check data location
LOCATION="$(bashio::config 'data_location')"
@@ -82,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

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

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

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)