You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Security
Creating an automation that performs a critical action now requires the PIN.add_automation wrote automation YAML and reloaded Home Assistant without ever passing the critical-action gate: _is_critical_action inspects a tool call's domain/service arguments, and this tool's payload is opaque automation_yaml. "Unlock the front door whenever I get home" therefore installed a lock.unlock automation with no PIN, while the direct command "unlock the front door" was gated — a durable bypass rather than a one-off action, since the installed automation keeps firing. Both adversarial reviewers converged on this independently during the v3.20.2 ship, where the automation-intent force-binding made the tool reliably reachable from everyday phrasings. With the PIN configured, a critical automation is now held as a pending action and the model is asked for the PIN exactly as it is for a direct unlock. Nothing touches automations.yaml or triggers a reload until the PIN is confirmed. Installs without a PIN are unaffected.
Screening is an allowlist over Home Assistant's own action taxonomy, not a list of blocked service names. A service call is not the only way an automation can unlock a door, and the first version of this gate — which only understood action:/service: steps — was defeated by four verified bypasses during pre-landing review, each confirmed against a real Home Assistant install by two independent reviewers. Every step is now classified with cv.determine_script_action and waved through only when it is provably inert or resolves to something no rule matches. Newly covered: device actions (device_id + domain + type), which carry no service name at all yet map straight to lock.unlock; scene.apply, which carries target states inline and reproduces an unlocked lock by calling lock.unlock (scene.create only snapshots state, but the scene it stores can be activated later, so it is screened too); and homeassistant.turn_on / turn_off / toggle, now screened against each target entity's own domain. An action type a future Home Assistant release adds is gated rather than skipped, so the screen degrades to over-prompting instead of to a hole.
Screening fails closed on anything it cannot resolve — a templated service name; a target that is an area, device, label, floor, group, or entity registry ID (both of which look like ordinary strings but resolve to entities named nowhere in the config); entity_id: all; and indirection through a scene, script, another automation, or a fired event. Expect a PIN prompt when an automation calls a script or activates a scene, even a harmless one: what those do lives in configuration this check cannot see, and guessing is what the gate exists to avoid. Screening runs after Home Assistant validates the automation, so blueprints are substituted and both service:/action: spellings normalized, and every nested branch is walked at any depth. Notification action labels nested in a step's data are still not mistaken for service calls.
cover.toggle and cover.set_cover_position now count as opening a door. The recommended critical actions listed only open_cover and open, but toggling a closed garage door opens it just as surely. lock.toggle and garage_door.toggle were added for the same reason. This closes the hole for direct commands as well as automations, so a household with the PIN enabled may now see a prompt for a phrasing that previously went through silently.
Device actions are screened on their domain, not on their type string. A device action's type is not the service it calls — each integration maps it in its own code, and cover turns set_position into cover.set_cover_position. Matching the raw type therefore only worked for locks, where the two names coincide by accident, leaving every cover device action open; a type: set_position with position: 100 opened a garage door with no PIN. A device action on a domain any rule guards is now gated whenever its type does not match a rule outright. This also covers user-added rules for domains this project ships no defaults for.
A resolvable target no longer masks an unresolvable one beside it.homeassistant.toggle aimed at [light.kitchen, <lock registry ID>] screened only the light and passed. Failing closed on the unresolvable remainder is now scoped by service name, so an area-wide homeassistant.turn_on — an everyday automation — still does not prompt.
The generic domain cannot be used to reach a script or scene.homeassistant.turn_on aimed at script.unlock_front_door forwards to script.turn_on and runs it, but a script.* target was treated as merely unresolvable, and unresolvable targets were gated only when the service name itself was guarded — no rule names turn_on. Targeting a script, scene, or automation entity is now indirection whatever the service is, matching how a direct script.turn_on call is already treated.
Services that run stored configuration are gated: python_script (whose sandbox permits hass.services.call), shell_command, rest_command, button.press / input_button.press (a template button's press field is a full script), and conversation.process (free text dispatches an intent, and an intent_script can unlock a door). Each has a device-action spelling as well, which is gated the same way.
Any generic homeassistant.* call whose targets are not all named entities is gated. An earlier iteration only gated unresolvable targets when the service name itself was guarded, to avoid prompting on an area-wide homeassistant.turn_on. That was unsound: the generic domain forwards by resolved domain, and an area, group, or registry ID can resolve to a script or to a template entity whose turn_on runs one. Screening happens before the automation is written and has no entity registry to consult, so it now asks rather than guesses. data_template, still accepted by Home Assistant, is inspected for targets alongside target and data.
An automation cannot be installed while the PIN is enabled but unset. The direct-command guard allows a call in that state with a logged warning; this path refuses instead and tells the user to set a PIN, because an automation persists and keeps firing rather than acting once.
The rule-matching semantics behind both gates live in one shared helper so the direct-command guard and the automation screen cannot drift apart. The test that pins them now asserts an absolute expected verdict on each side rather than merely that the two agree — the earlier version stayed green with the shared matcher stubbed out entirely.
Fixed
A confirmed automation is claimed before it is written, so two confirmations of the same action in one concurrent tool batch cannot install it twice.
Abandoned PIN challenges no longer accumulate. Expired confirmations were only ever dropped when that exact action was later confirmed, so a challenge the model never resolved was retained for the life of the config entry — and once more than one piled up, the single-pending-action convenience path stopped resolving. Registering a confirmation now sweeps expired entries and caps the store.
Documentation corrected: the Critical Action PIN section stated that enabling the guard without setting a PIN causes the agent to reject requests. For direct commands it does not — it logs a warning and allows the action. The docs now describe the actual behavior of each path.
Known limitations
Raw protocol writes are not screened. Screening matches domains and services, so a call that addresses a device beneath the entity layer — mqtt.publish to a lock's command topic, zwave_js.set_value writing a Door Lock command class, zha.issue_zigbee_cluster_command — can reach a lock without naming the lock domain. These are deliberately not gated by default because they are routine on those stacks; docs/configuration.md documents a per-transport rule you can add to critical_actions if you run locks on one of them.
Targets that resolve at run time cannot be inspected. Screening happens before the automation is written and has no entity registry, so a group, area, or registry ID cannot be expanded and a template entity cannot be told apart from an ordinary one. Unresolvable targets are gated rather than reasoned about, which is why some automations prompt more than a human reader would expect.
A blueprint-based automation is stored as a use_blueprint: reference and re-substituted by Home Assistant on every reload, so the PIN attests to what the blueprint did at approval time. Editing that blueprint file afterwards changes what the approved automation runs without a fresh prompt. Plain YAML automations are written exactly as screened.