Skip to content

Commit 79546c6

Browse files
committed
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 homeassistant-ai#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 homeassistant-ai#936
1 parent b6198d5 commit 79546c6

4 files changed

Lines changed: 188 additions & 58 deletions

File tree

homeassistant-addon/DOCS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ The add-on provides 86+ MCP tools for controlling Home Assistant:
423423
- `ha_backup_create` — Create a fast Home Assistant backup (local only).
424424
- `ha_backup_restore` — Restore Home Assistant from a backup (LAST RESORT - use with extreme caution).
425425
- `ha_check_config` — Check Home Assistant configuration for errors.
426-
- `ha_config_set_yaml` Add, replace, or remove a top-level key in configuration.yaml or package files.
426+
- `ha_config_set_yaml` ESCAPE HATCH — raw YAML edit. Use only when NO dedicated tool fits.
427427
- `ha_get_system_health` — Get Home Assistant system health, including Zigbee (ZHA) and Z-Wave JS network diagnostics.
428428
- `ha_get_updates` — Get update information - list all updates or get details for a specific one.
429429
- `ha_reload_core` — Reload Home Assistant configuration without full restart.

site/src/data/tools.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2414,16 +2414,20 @@
24142414
},
24152415
{
24162416
"name": "ha_config_set_yaml",
2417-
"title": "Set YAML Config",
2418-
"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.",
2417+
"title": "Raw YAML Config Edit (Escape Hatch)",
2418+
"description": "ESCAPE HATCH — raw YAML edit. Use only when NO dedicated tool fits.\n\nThis tool is the WRONG answer for almost everything. Before calling it,\nconfirm that NONE of these apply:\n\n- Template sensors (state-based OR trigger-based) ->\n ha_set_config_entry_helper with domain='template'. The Template\n config-flow helper supports triggers, availability, attributes, device\n class, unit of measurement, and state templates since HA 2024.x. It is\n the correct path even for complex trigger-based sensors. Do NOT edit\n the 'template:' YAML block for this.\n- Automations -> ha_config_set_automation\n- Scripts -> ha_config_set_script\n- Scenes -> ha_config_set_scene\n- Input helpers (input_boolean, input_number, input_text, input_select,\n input_datetime, input_button, counter, timer, schedule) ->\n ha_config_set_helper\n- Groups, min/max, threshold, derivative, statistics, utility_meter\n entities that can be created as helpers -> ha_config_set_helper\n\nThis tool is intended for YAML-only integrations that have no config-flow\nor API equivalent: command_line sensors, REST sensors defined in\npackages, shell_command entries, platform-based notify services, and\nsimilar edge cases. If you are reaching for this to edit 'template:',\n'automation:', 'script:', 'scene:', or 'input_*:', stop and use the\ndedicated tool instead.\n\nA ``justification`` is REQUIRED on every call and is logged for auditing.\nThe justification must explain why no dedicated tool fits. If you cannot\nwrite a concrete justification, you are using the wrong tool.\n\nSafeguards: file backup, YAML validation, top-level key allowlist,\npath traversal blocking, post-edit config check.\n\nCheck 'post_action' in the response. Most keys require a full HA restart\n('restart_required'). Only template, mqtt, and group support reload\n('reload_available' with 'reload_service').\n\nPreserves YAML comments on sibling keys, file-level comments, and Home\nAssistant tags (!include, !secret, etc.). The 'replace' action\nsubstitutes the subtree as-is, so comments from the old subtree do not\ncarry over.",
24192419
"inputSchema": {
24202420
"properties": {
24212421
"yaml_path": {
2422-
"type": "Annotated[str, Field(description=\"Top-level YAML key to modify (e.g., 'template', 'sensor', 'input_boolean'). Only whitelisted keys are allowed.\")]"
2422+
"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', 'utility_meter'). STOP before using this for 'template' — the Template config-flow helper (ha_set_config_entry_helper with domain='template') supports state AND trigger-based template sensors since HA 2024.x and is the correct path. Do NOT use this tool for automations, scripts, scenes, or input_* helpers — they have dedicated tools (ha_config_set_automation, ha_config_set_script, ha_config_set_scene, ha_config_set_helper).\")]"
24232423
},
24242424
"action": {
24252425
"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).\")]"
24262426
},
2427+
"justification": {
2428+
"type": "Annotated[str | None, Field(default=None, description='Required. Explain in one or two sentences why no dedicated tool (ha_set_config_entry_helper for templates, ha_config_set_automation, ha_config_set_script, ha_config_set_scene, ha_config_set_helper) can accomplish this task. Logged for auditing. If you cannot write a concrete justification, you probably want a different tool.')]",
2429+
"default": null
2430+
},
24272431
"content": {
24282432
"type": "Annotated[str | None, Field(default=None, description=\"YAML content for the value under yaml_path. Required for 'add' and 'replace' actions. Must be valid YAML.\")]",
24292433
"default": null

src/ha_mcp/tools/tools_yaml_config.py

Lines changed: 92 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def register_yaml_config_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
4848
annotations={
4949
"destructiveHint": True,
5050
"idempotentHint": False,
51-
"title": "Set YAML Config",
51+
"title": "Raw YAML Config Edit (Escape Hatch)",
5252
},
5353
)
5454
@log_tool_usage
@@ -57,8 +57,17 @@ async def ha_config_set_yaml(
5757
str,
5858
Field(
5959
description=(
60-
"Top-level YAML key to modify (e.g., 'template', 'sensor', "
61-
"'input_boolean'). Only whitelisted keys are allowed."
60+
"Top-level YAML key to modify. Only a narrow allowlist of "
61+
"YAML-only integration keys is accepted (e.g., 'command_line', "
62+
"'rest', 'shell_command', 'notify', 'utility_meter'). "
63+
"STOP before using this for 'template' — the Template "
64+
"config-flow helper (ha_set_config_entry_helper with "
65+
"domain='template') supports state AND trigger-based template "
66+
"sensors since HA 2024.x and is the correct path. Do NOT use "
67+
"this tool for automations, scripts, scenes, or input_* "
68+
"helpers — they have dedicated tools "
69+
"(ha_config_set_automation, ha_config_set_script, "
70+
"ha_config_set_scene, ha_config_set_helper)."
6271
),
6372
),
6473
],
@@ -72,6 +81,20 @@ async def ha_config_set_yaml(
7281
),
7382
),
7483
],
84+
justification: Annotated[
85+
str | None,
86+
Field(
87+
default=None,
88+
description=(
89+
"Required. Explain in one or two sentences why no dedicated "
90+
"tool (ha_set_config_entry_helper for templates, "
91+
"ha_config_set_automation, ha_config_set_script, "
92+
"ha_config_set_scene, ha_config_set_helper) can accomplish "
93+
"this task. Logged for auditing. If you cannot write a "
94+
"concrete justification, you probably want a different tool."
95+
),
96+
),
97+
] = None,
7598
content: Annotated[
7699
str | None,
77100
Field(
@@ -103,30 +126,48 @@ async def ha_config_set_yaml(
103126
),
104127
] = True,
105128
) -> dict[str, Any]:
106-
"""Add, replace, or remove a top-level key in configuration.yaml or package files.
129+
"""ESCAPE HATCH — raw YAML edit. Use only when NO dedicated tool fits.
130+
131+
This tool is the WRONG answer for almost everything. Before calling it,
132+
confirm that NONE of these apply:
107133
108-
IMPORTANT: Only use when NO UI or API alternative exists. Prefer:
109-
- Template sensors -> ha_config_set_helper (Template Helper)
134+
- Template sensors (state-based OR trigger-based) ->
135+
ha_set_config_entry_helper with domain='template'. The Template
136+
config-flow helper supports triggers, availability, attributes, device
137+
class, unit of measurement, and state templates since HA 2024.x. It is
138+
the correct path even for complex trigger-based sensors. Do NOT edit
139+
the 'template:' YAML block for this.
110140
- Automations -> ha_config_set_automation
111141
- Scripts -> ha_config_set_script
112-
- Input helpers -> ha_config_set_helper
113142
- Scenes -> ha_config_set_scene
143+
- Input helpers (input_boolean, input_number, input_text, input_select,
144+
input_datetime, input_button, counter, timer, schedule) ->
145+
ha_config_set_helper
146+
- Groups, min/max, threshold, derivative, statistics, utility_meter
147+
entities that can be created as helpers -> ha_config_set_helper
114148
115-
This tool is for YAML-only features with no UI/API path (e.g.,
116-
command_line sensors, platform-based MQTT sensors in YAML, rest
117-
sensors defined in packages).
149+
This tool is intended for YAML-only integrations that have no config-flow
150+
or API equivalent: command_line sensors, REST sensors defined in
151+
packages, shell_command entries, platform-based notify services, and
152+
similar edge cases. If you are reaching for this to edit 'template:',
153+
'automation:', 'script:', 'scene:', or 'input_*:', stop and use the
154+
dedicated tool instead.
118155
119-
Safeguards: file backup, YAML validation, top-level key whitelist,
156+
A ``justification`` is REQUIRED on every call and is logged for auditing.
157+
The justification must explain why no dedicated tool fits. If you cannot
158+
write a concrete justification, you are using the wrong tool.
159+
160+
Safeguards: file backup, YAML validation, top-level key allowlist,
120161
path traversal blocking, post-edit config check.
121162
122-
IMPORTANT: Check 'post_action' in the response. Most keys require
123-
a full HA restart ('restart_required'). Only template, mqtt, and
124-
group support reload ('reload_available' with 'reload_service').
163+
Check 'post_action' in the response. Most keys require a full HA restart
164+
('restart_required'). Only template, mqtt, and group support reload
165+
('reload_available' with 'reload_service').
125166
126-
Preserves YAML comments on sibling keys, file-level comments,
127-
and Home Assistant tags (!include, !secret, etc.). The 'replace' action
128-
substitutes the subtree as-is, so comments from the old subtree
129-
do not carry over.
167+
Preserves YAML comments on sibling keys, file-level comments, and Home
168+
Assistant tags (!include, !secret, etc.). The 'replace' action
169+
substitutes the subtree as-is, so comments from the old subtree do not
170+
carry over.
130171
"""
131172
try:
132173
# Validate action
@@ -144,6 +185,31 @@ async def ha_config_set_yaml(
144185
)
145186
)
146187

188+
# Require a non-empty justification. This is the primary friction
189+
# that discourages reaching for this escape hatch — if the caller
190+
# cannot articulate why no dedicated tool fits, they should use a
191+
# dedicated tool instead. Mirrors the pattern from tools_code.py
192+
# (ha_manage_custom_tool).
193+
if not justification or not justification.strip():
194+
raise_tool_error(
195+
create_error_response(
196+
ErrorCode.VALIDATION_INVALID_PARAMETER,
197+
"justification is required for ha_config_set_yaml",
198+
suggestions=[
199+
"Explain why no dedicated tool can accomplish this "
200+
"task (Template sensors -> "
201+
"ha_set_config_entry_helper with "
202+
"domain='template'; automations -> "
203+
"ha_config_set_automation; scripts -> "
204+
"ha_config_set_script; helpers -> "
205+
"ha_config_set_helper)",
206+
"If yaml_path is 'template', 'automation', "
207+
"'script', 'scene', or 'input_*', use the "
208+
"dedicated tool instead of this escape hatch",
209+
],
210+
)
211+
)
212+
147213
# Validate content is provided for add/replace
148214
if action in ("add", "replace") and not content:
149215
raise_tool_error(
@@ -156,6 +222,14 @@ async def ha_config_set_yaml(
156222
)
157223
)
158224

225+
logger.info(
226+
"ha_config_set_yaml invoked (yaml_path=%s, action=%s, file=%s) — justification: %s",
227+
yaml_path,
228+
action,
229+
file,
230+
justification[:200],
231+
)
232+
159233
# Coerce boolean parameter
160234
backup_bool = coerce_bool_param(backup, "backup", default=True)
161235

0 commit comments

Comments
 (0)