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>
This commit is contained in:
alexbelgium
2026-07-15 14:24:17 +02:00
parent e5983f4718
commit 0223cc3511
2 changed files with 35 additions and 12 deletions

View File

@@ -294,7 +294,7 @@ import re
from pathlib import Path from pathlib import Path
path = Path(os.environ["CLAUDE_MD"]) path = Path(os.environ["CLAUDE_MD"])
text = path.read_text() text = path.read_text(encoding="utf-8")
pattern = re.compile( pattern = re.compile(
r"\n*<!-- BEGIN ha-api-helper \(managed by claude_desktop addon\) -->.*?" r"\n*<!-- BEGIN ha-api-helper \(managed by claude_desktop addon\) -->.*?"
r"<!-- END ha-api-helper \(managed by claude_desktop addon\) -->\n?", r"<!-- END ha-api-helper \(managed by claude_desktop addon\) -->\n?",
@@ -302,7 +302,7 @@ pattern = re.compile(
) )
new = pattern.sub("", text) new = pattern.sub("", text)
if new != text: if new != text:
path.write_text(new) path.write_text(new, encoding="utf-8")
PY PY
fi fi

View File

@@ -9,10 +9,15 @@ therefore never reachable.
Authentication and the base URL are resolved automatically, in this order: Authentication and the base URL are resolved automatically, in this order:
1. $HA_BASE_URL + $HA_TOKEN explicit override (advanced/scoped) 1. $HA_BASE_URL + $HA_TOKEN explicit override (advanced/scoped)
2. $SUPERVISOR_TOKEN Supervisor Core-API proxy 2. `ha_mcp_token` in /data/options.json scoped user long-lived token -> :8123
(needs homeassistant_api: true, 3. $SUPERVISOR_TOKEN Supervisor Core-API proxy fallback
which this add-on sets — zero setup) (admin-equivalent; needs
3. `ha_mcp_token` in /data/options.json user long-lived token -> :8123 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: Subcommands:
ha-cli get <path> GET e.g. get config/automation/config/1700000000 ha-cli get <path> GET e.g. get config/automation/config/1700000000
@@ -49,19 +54,26 @@ def _load_option(name):
return None 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(): def resolve_endpoint():
"""Return (rest_base, ws_url, token) for the best available auth path.""" """Return (rest_base, ws_url, token) for the best available auth path."""
base = os.environ.get("HA_BASE_URL") base = os.environ.get("HA_BASE_URL")
token = os.environ.get("HA_TOKEN") token = os.environ.get("HA_TOKEN")
if base and token: if base and token:
rest = base.rstrip("/") rest = base.rstrip("/")
if not rest.endswith("/api"):
rest += "/api"
ws = rest.replace("http", "ws", 1).rsplit("/api", 1)[0] + "/api/websocket" ws = rest.replace("http", "ws", 1).rsplit("/api", 1)[0] + "/api/websocket"
return rest, ws, token return rest, ws, token
token = os.environ.get("SUPERVISOR_TOKEN") # Checked before SUPERVISOR_TOKEN: this add-on always sets homeassistant_api,
if token: # so the admin-equivalent Supervisor token is otherwise always present and
return "http://supervisor/core/api", "ws://supervisor/core/websocket", token # would shadow a user's deliberately scoped-down ha_mcp_token.
token = _load_option("ha_mcp_token") token = _load_option("ha_mcp_token")
if token: if token:
return ( return (
@@ -70,9 +82,14 @@ def resolve_endpoint():
token, token,
) )
token = os.environ.get("SUPERVISOR_TOKEN")
if token:
return "http://supervisor/core/api", "ws://supervisor/core/websocket", token
sys.exit( sys.exit(
"ha-cli: no credentials. Expected $SUPERVISOR_TOKEN (default inside the " "ha-cli: no credentials. Expected ha_mcp_token in the add-on options "
"add-on), or $HA_BASE_URL+$HA_TOKEN, or 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."
) )
@@ -170,6 +187,12 @@ def main(argv):
print(__doc__) print(__doc__)
return 0 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() rest_base, ws_url, token = resolve_endpoint()
cmd, args = argv[0], argv[1:] cmd, args = argv[0], argv[1:]