atm_30_seconds.mp4
ATM gives your AI agents scoped, least-privilege access to Home Assistant. Each client gets its own token, limited to exactly the entities you allow, with its own capabilities, rate limit, and optional expiry. Every request is audited, any token can be revoked instantly, and the per-entity semantic safety layer (MESA) can make a device confirm-only or off-limits by its nature, no matter what permissions a token is granted.
ATM runs entirely inside Home Assistant, with no extra process, no cloud dependency, and no setup beyond the ATM panel. It works with the MCP clients you already use (Claude Code, Cursor, ChatGPT/Codex, Gemini, and others), or you can skip installing a client altogether and chat from inside Home Assistant itself. Either way, a guided setup takes you from a new token to a working agent in minutes, backed by a catalog of 98 tools for reading, controlling, and authoring your configuration.
The full documentation, including the tool reference, permissions, capabilities, MESA, and the admin API, lives at sfox38.github.io/ATM.
New here? Start with the Quick start: it creates your first token, grants it one device, then lets you choose whether to chat from inside Home Assistant or connect an external agent, in a few minutes either way.
We road-tested ATM against the built-in and community MCP servers, one agent model, one synthetic home, the same tasks. ATM was the cheapest per completed task, the only server that left every off-limits device untouched, and the most capable. See the latest road test report, or run the tests yourself from the roadtest repository.
- Home Assistant 2024.5.0 or later.
- One ATM instance per Home Assistant. No Python dependencies beyond what HA ships.
- In HACS, open Integrations, then the top-right menu, and choose Custom repositories.
- Enter
https://github.com/sfox38/ATMand select Integration as the category, then click Add. - Find ATM in the HACS integration list, install it, and restart Home Assistant.
Prefer to install by hand? Copy the custom_components/atm folder into your Home Assistant config directory under custom_components/atm, then restart.
Go to Settings > Devices & services > Add integration and search for Advanced Token Management. Click through the single-step config flow, then open the ATM panel in your sidebar. The Quick start takes it from there.
The complete release history is on the GitHub releases page. Recent changes:
- New: Bundled mesa-core updated to 1.2.0
- New: Agent Chat, our recommended way to use ATM. Instead of using an external client, you can now talk to Home Assistant from a floating chat window. ATM runs the agent itself, on a token you choose, so every capability gate, per-entity MESA rule, approval, and audit entry applies exactly as it would for any external MCP client.
- New: use ATM with Home Assistant's own Assist and voice. You can now bind an ATM token to Home Assistant's native assistant so a voice or chat conversation runs through that token's scope, MESA safety, approvals, and audit. A least-privilege token (for example the Voice Assistant persona) is recommended. A Confirm-gated action becomes queued for approval in the ATM panel, since voice has no inline Approve button.
- New: ATM as a Home Assistant conversation agent (self-contained voice). The option above hands ATM's tools to a model supplied by a separate integration (OpenAI, Anthropic, Ollama, and so on). ATM can register itself directly as a conversation agent so "ATM" appears in Settings > Voice assistants and runs its own model, an existing Agent Chat provider account, on a token's scope, with no separate integration. ATM can then set up the assistant for you in one click (it creates an Assist assistant pointed at ATM). Confirm-gated actions are queued for approval in the ATM panel (voice has no inline Approve button).
- New: ATM as a Home Assistant AI Task entity. Home Assistant's AI Task lets automations, scripts, and dashboards generate text or structured data with an AI model (the
ai_task.generate_dataaction). ATM can now provide that entity itself, and it runs ATM's own model on the token's scope, with the same per-token permissions, MESA safety, approvals, and audit as the rest of ATM. - New:
mesa_get_profilecan include an entity's "semantic moments", the purpose-specific triggers and conditions Home Assistant 2026.7 introduced (e.g. a lock's "locked" trigger), so an agent reading a MESA profile also sees what automation moments the entity offers. Passinclude_semantic_moments: true; the list is read live from Home Assistant, is informational only, and is simply omitted on older Home Assistant versions. For moments whose config shape is non-obvious (such as a numeric-threshold crossing), each moment also includes a ready-to-adapt example config, so an agent can copy the exact automation trigger or condition rather than reverse-engineering it. The connection primer tells agents that can author automations to call it on the trigger or condition entity before building the automation. - New: MESA advisory leases. Two new tools,
mesa_request_leaseandmesa_release_lease, let an agent announce "I am operating these entities for the next few seconds" (30-second cap) so MESA-aware components can coordinate. A lease grants no authority (every action stays gated by scope, capabilities, and MESA), covers only entities the token could already control, is denied for entities under protected or critical automation control, and lease endings fire anatm_mesa_lease_expiredevent you can automate on. - New: MESA profile export and import. Two icon buttons next to the MESA tab's refresh control download your entire MESA profile set as one JSON file (the standard MESA archive format, so it doubles as a backup and transfers to any other MESA-aware server) and import one back. Both confirm first; importing validates every profile, reports anything invalid without writing it, and by default never touches an existing profile. A "Replace existing profiles" checkbox switches to overwrite mode behind a clearly destructive confirm.
- New: the on-boarding wizard now offers Agent Chat as its primary client - external clients are no longer required. After granting access to one device, a new "Choose how to connect" step lets you pick chatting inside Home Assistant or connecting an external app. Choosing Agent Chat walks you through adding a model provider account right there in the wizard, then a highlighted "Try now" opens the Agent Chat window with your new token and provider already selected and a test prompt typed in for you.
- New: Include-graph-aware YAML authoring. The automation, script, and scene tools now follow
configuration.yaml's!include/!include_dir_*layout to the correct leaf file and edit only the target entry's lines, preserving comments, formatting, and!secretreferences everywhere else, instead of refusing whenever an include appeared. Creates route by include flavor (append for plain!include, a new file per entry for directory includes); every write is re-parse-verified before it lands, and anything ambiguous (duplicate ids, packages/inline config, multiple create targets) refuses rather than guesses. Even HA's own UI editor still cannot edit split configurations at all. - New: per-token MCP server names. The connect instructions now derive the client-side server name from the token's name (
atm-<token-name>) instead of one shared name, so you can keep one client entry per token and switch tokens by toggling which entry is enabled in your MCP client, no more clearing auth and retyping. Enable only one atm-* entry at a time. - New: token settings presets (experimental, off by default). A token can save named snapshots of its full configuration (capabilities, permissions, rate limits, pass-through) and switch between them from its detail view. Turning the setting on saves every token's current settings as its first preset; switching first saves your unsaved changes into the outgoing preset (the dialog shows exactly what), and applying the active preset reverts to its saved state, so nothing is ever lost silently. Switching is admin-only by design.
- New: inline "wait for approval" per-token setting. When a Confirm-gated action is queued, ATM now holds the response open for a short window so that if you approve within it, the agent gets the result in the same call with no second round-trip; if you do not decide in time it falls back to the immediate "pending approval" reply and the agent carries on. On by default at 60 seconds, adjustable per token from 30 to 180 seconds in the token's detail view. Agents that run unattended (no human watching) can be switched to the return-immediately behavior via the admin API.
- New: Zigbee management. Agents can now see radio-network health and, when granted, manage the Zigbee network. Two read tools under the diagnostics capability (
get_radio_networkfor channel, coordinator, and join-window state;get_radio_devicefor one device's signal quality, availability, and mesh neighbors) and three write tools under a new "Radio management" capability (permit_zigbee_jointo open or close the pairing window,reconfigure_zigbee_deviceto re-interview a misbehaving device,remove_zigbee_deviceto drop one from the network), all Confirm-eligible. Works with Zigbee2MQTT and ZHA. Network keys and other credentials never appear in any response; device writes require write access to the device's entities; the coordinator itself can never be targeted. Persona defaults: deny everywhere except confirm for power_user, home_admin, and maintenance. - New: agents are told when their tool list goes stale. ATM's stateless MCP transport cannot push a tools-changed notification, so after you change a token's capabilities, permissions, or preset mid-session, the next tool response carries a one-time notice telling the agent to check get_capability_summary and ask you to reconnect. The audit log marks the request that carried the notice and records the token's active preset per request.
- New: MESA profile suggestions. The MESA tab now scans for under-protected entities and suggests profiles you can apply in one click, review in the editor, or dismiss: automations, scripts, and scenes whose actions reach risky devices (an agent allowed to trigger them controls those devices indirectly, bypassing the devices' own profiles), and risky devices (locks, alarms, garage doors, valves, sirens, water heaters, firmware updates) with no MESA coverage at any level. Locks and alarms suggest prohibited (matching the built-in baseline); everything else suggests confirm. Suggestions are never applied automatically, and the scan is honest about being a lower bound: templated automation targets are invisible to it.
- New: typo-tolerant entity search.
search_entitiesmatched only exact words, so a misspelled query ("themostat", "kitchne") returned nothing. It now falls back to an approximate, similarity-based match when, and only when, the exact search finds nothing, returning the closest entities and flagging the result as approximate. Queries that already match exactly behave exactly as before. - New: MESA profiles can now use solar-angle time conditions ("only after sunset", twilight boundaries, with minute offsets). They are evaluated against Home Assistant's own sun position calculations; previously these conditions were always treated as unevaluable and failed closed, and they still fail closed if the sun position cannot be computed.
- New: when an agent calls a service by the wrong name on a common device domain (for example
valve.openinstead ofvalve.open_valve, orcover.openinstead ofcover.open_cover), ATM now tells it the valid service names for that domain instead of a bare "Forbidden.", so it can retry correctly rather than assuming it lacks permission. This reveals nothing sensitive: it only ever fires for a device the token already controls, and the suggested names are a fixed list of standard Home Assistant services, never a lookup of what is actually installed. Every other case still returns the generic error, so ATM never discloses whether an arbitrary service exists. - New: agents can now call Home Assistant's config-reload services (
automation.reload,script.reload,scene.reload,homeassistant.reload_all, and the other core reloads for helpers, groups, zones, people, and schedules) throughcall_service. These take no entity target. The reload is Confirm-eligible like any other write. This is a curated list of stable core reloads; integration-specific reloads are intentionally not included. - Panel: a rejected approval now shows a readable reason instead of a raw code. An approval that failed re-execution shows the underlying tool error (e.g. "Execution failed: Forbidden.") rather than the bare
execution_failedslug, and the other internal reason codes render as plain English; a free-text reason an admin typed still shows verbatim. - Panel: the Approvals list now updates instantly when a request is queued or resolved (it subscribes to the Home Assistant event bus, matching the badge) instead of lagging behind on its own poll.
- Panel: the approval before/after diff is now a red/green line diff (removed lines tinted red, added green) in a wider dialog with an icon toggle between side-by-side and stacked layout, matching the Changes tab, so it is easy to see exactly what an approval would change. Raw args, results, and audit payloads render in Home Assistant's code editor (which now also upgrades to the editor if HA registers it after the panel loads, instead of staying on the plain-text fallback for the session).
- Panel: the Changes (version history) tab now shows structured configs (automations, scripts, scenes, helpers, dashboards) as a red/green line diff, the same as raw file snapshots, instead of two plain side-by-side panes.
- Panel: delete approvals now show the configuration being removed as an all-red pane, so you can see exactly what a delete would remove before approving.
- Panel: Audit, Approvals, and Changes now paginate the same way: a page at a time with a "Load more" button.
- Panel: an approval or audit entry for a MESA-gated action (one blocked by a device's own safety setting rather than a token capability) now shows "MESA-gated action" instead of the internal
call_service_mesa_approvedlabel, which read like a real tool name but was not one you could actually call. - Panel: accessibility and responsiveness. The light theme's amber accent and muted-grey text were darkened one shade to meet WCAG AA contrast; form fields and the Agent Chat window gained proper labels and polite screen-reader announcements; keyboard focus is now visible on the capability mode selectors; dialogs keep focus contained and move it to the new step on wizard and modal transitions. Responsive frontend now works on any mobile device.
- Changed: valve mutation (open / close / stop / set position) now requires the physical-control capability, the same as covers, locks, and alarms. A valve controls the flow of water or gas and is just as consequential as a cover, so gating it consistently closes a gap where a valve could be opened without that capability. If you have a token that operates valves, make sure it has physical control enabled (allow or confirm), not denied.
- Changed: per-token sensors (request count, last access) now publish to Home Assistant at most once every 5 seconds per token instead of on every single request. A busy agent previously generated one recorder row per call in pure telemetry; panel statistics are unaffected (they read the live counters directly) and automations watching an ATM sensor see at most 5 seconds of extra latency.
- Changed: changing the audit flush interval in Settings now takes effect immediately instead of after the previously scheduled flush wakes up, and the "Never" setting no longer keeps an idle timer running.
- Changed: the Settings > Data Management wipe is now scoped by three independent switches: all ATM data (tokens, archived records, permissions, settings, and the audit log), your Agent Chat provider accounts and their stored API keys, and your MESA safety profiles. The first two default on and MESA defaults off, so a reset keeps hand-authored safety policy unless you ask otherwise. The wipe also tears down any queued pending approvals and dismisses their notifications, the same cleanup revoking a token performs.
- Behavior change: on a configuration.yaml that does not route automations/scripts/scenes to an editable YAML file, create now refuses with a clear message; previously it silently wrote a hardcoded file Home Assistant never loaded.
- Hardened: the MCP endpoint now rejects malformed JSON-RPC requests cleanly instead of returning a server error, validates the
Originheader against your Home Assistant URLs as a DNS-rebinding defense, and can no longer have its audit log poisoned by a malformed request. - Hardened: the Home Assistant compatibility probe now also catches a class of constructor drift it previously missed (required keyword-only parameters); a new startup check warns when the system_log internals get_logs depends on have changed shape; a broken template-sandbox environment is now a loud startup warning; settings writes are clamped and type-coerced at the storage layer on every path, not just the admin API; the pass-through entity-filter logic that was hand-written in four places is now a single shared path; and a batch of dead code and unused constants was removed.
- Security: The physical-control capability now gates on the actuator domain rather than an exact service-name list, so
cover.toggle, cover/valve tilt services,valve.toggle,alarm_control_panel.alarm_arm_custom_bypass, andhomeassistant.toggleon a cover or valve are all covered (previously these could actuate a lock/cover/valve/alarm on a token that had write access butcap_physical_controldenied). Non-finite JSON numbers (NaN/Infinity) in tool arguments are now rejected instead of silently passing range checks. Audit-log fields are now length-bounded and type-coerced so a malformed value can't bloat or poison a stored entry. Thesearch_entitiesfree-text query is now length-capped. - Fixed: a native intent tool (
HassTurnOn,HassTurnOff, and the otherHass*tools) called with no recognized target (no name, area, floor, domain, or device class) no longer falls back to every entity the token can write. This could be triggered by a model passing an unrecognized argument such asentity_id(these tools takename, notentity_id), which left every recognized selector empty; the call now matches nothing (fail closed) instead of fanning the action out to the token's whole writable scope. A call with any real selector (including a domain-only "all the lights") is unaffected. - Fixed: calling a service the token is genuinely permitted to use, but with a value Home Assistant itself rejects (for example a thermostat setpoint outside the entity's allowed range), is now reported to the agent as an invalid request with the actual reason, instead of the generic "Forbidden." that made a bad value look like a permission denial. This only applies to Home Assistant's own input-validation errors on an entity the token already controls; a missing/unknown service still returns the generic error so service existence is never revealed.
- Fixed: an agent targeting entities by name with an unexpected argument shape (for example asking to lock two named locks and sending a list of names instead of one) could previously hang for minutes instead of getting an error back. The underlying issue is fixed (a list is now treated the same as if the name had not been given, so the request still resolves via any other target you specified), and every tool call now has a safety net that turns any unexpected internal error into a normal, fast response instead of leaving the connection hanging.
- Fixed: the native
HassTurnOn/HassTurnOfftools now route vacuum and valve entities to their real domain services (vacuum.start/stop,valve.open_valve/close_valve), the same routing locks and covers already had. Home Assistant's generichomeassistant.turn_on/turn_offsilently ignores these domains (it logs "does not support entities vacuum.X" and does nothing), so before this fix the tool reported success while the device never moved. - Fixed:
HassSetPositionnow positions valves as well as covers, dispatching each target to its own domain service (cover.set_cover_position/valve.set_valve_position). The tool was previously hardcoded to covers, so a valve could never be resolved or positioned through it even though the documentation always promised valve support. - Fixed: the per-token sensor write debounce (
flush_sensor_writes) could be dispatched to Home Assistant's executor thread pool instead of the event loop. The function is now correctly marked@callbackso it always runs on the event loop, matching every other scheduled callback in the integration. - Fixed: a larger request body (such as importing a well-profiled deployment's MESA archive, or a big automation/dashboard/file argument on any MCP tool call) could occasionally fail with "Invalid JSON body." even though it was sent correctly; every request reader in ATM (admin API, REST proxy, and the MCP endpoint) now keeps reading until the whole body has arrived instead of trusting a single read to return everything at once.
- Fixed: revoking a token (or a token expiring) now also cancels its queued pending approvals, dismisses their notifications, and fires the resolved event for each, so a dead token's requests no longer sit in the Approvals queue looking approvable until they time out. An approval that is mid-execution at that moment is left alone. The audit outcome filter in the panel also gains Invalid Request and Pending Approval options, and a pending-approval audit entry is now documented as always recorded (logging toggles cannot suppress the approval trail).
- Fixed: the get_relationships tool no longer reads YAML files on Home Assistant's event loop (it now uses a worker thread, like describe_entity always did), avoiding the blocking-call warnings recent Home Assistant versions log for this.
- Fixed: an entry containing
!secretno longer breaks the authoring tools (the file previously failed to load at all). Edits replace an entry wholesale, so a dropped!secretreference is visible in the version history and approval diff; restoring a version whose snapshot contains YAML tags is refused to avoid writing a broken config. - Fixed: the
set_dashboard_configapproval showed an empty "Before" pane (the diff builder was synchronous and could not read the current layout). It now reads the dashboard's current layout for the "Before" side, with out-of-scope entity ids redacted the same wayget_dashboard_configdoes, so an edit shows a real before/after. - Fixed: a malformed or corrupted tool call (for example from a struggling local model) could write an unreadably long string into the audit log's Method/Resource columns. It is now capped to a sane length; no real tool call is affected.
- Fixed:
search_entities's "only show stale entities" filter (stale_hours) failed every time it was used, with a generic internal error. It now correctly compares each entity's last-changed time. - Fixed: creating or editing an automation, script, scene, helper, dashboard layout,
configuration.yaml, or a scoped file with an invalid config used to still show "queued for approval" if the capability was set to Confirm. These tools now check the same way they would eventually fail before queuing the approval at all, so an invalid request is rejected immediately and never shows as pending. - Fixed: calling a service whose own Home Assistant schema does not support an entity target at all, such as a notification service, is a different problem than lacking permission for one, and previously failed with a generic internal error instead of a clear explanation. The agent now gets the actual reason back and can adjust its request instead of retrying blindly.
- Fixed:
search_entitiessilently returned zero results wheneverdomain,device_class, orstatewas passed as an empty string rather than omitted, instead of treating it the same as not filtering on that field at all. Some MCP clients fill in every declared optional parameter on every call rather than omitting unset ones, which made an otherwise-correct search return nothing for no visible reason. - Fixed: an action gated by both a Confirm-mode capability and a MESA confirm-mode entity erroneously required two sequential admin approvals: approving the first quietly queued a second "MESA confirmation" approval for the same action. Entities whose MESA nature is prohibited or read-only are still refused regardless of approvals. So that MESA's involvement stays on the record with no second approval, such an approval is now marked "(includes MESA confirmation)" in its summary (visible in the Approvals list and History) and its review card names the entities that confirmation covers.
- New: Raw
configuration.yamland file writes (set_yaml_config,write_file) are now captured in the configuration version history with admin rollback, the same before/after snapshots and Restore the structured authoring tools already had. - New: Orphaned MESA profiles can be cleared in bulk: a Clear all orphaned profiles button on the MESA tab removes every profile whose entity, area, or integration no longer exists in one step
- New: The agent skill guide now includes modular domain-authoring recipes (automations, scripts and scenes, dashboards and cards, conditional/visibility, and climate), so connected agents produce better Home Assistant configuration.
- New:
get_logbook(the human-readable event history) - New:
get_calendar_events(events from an accessible calendar entity within a time window) - New:
list_blueprintstool lists installed automation and script blueprints with their inputs - New: entity-registry tools
set_entity(friendly name / icon / area) anddelete_entity(remove a stale or duplicate registry entry; renaming anentity_idis intentionally not exposed. - Security fix:
get_system_healthnow scrubs secret-keyed values and URL-embedded credentials from the per-integration health data before returning it, and also network-topology details (LAN/loopback/link-local IP addresses, hostnames inside URLs, and filesystem paths), consistent with whatget_configalready withholds. The topology scrub is conservative (private IP ranges only, so a public-IP-shaped version string is untouched). - Security fix:
validate_configno longer reveals whether an out-of-scope entity exists. - Fix: The MCP batch endpoint now runs its items sequentially instead of concurrently, so a single batch can never execute many write tools at once and interleave them.
- Fix:
get_confignow returns a curated subset of Home Assistant's configuration (version, time zone, units, location name, loaded components) instead of the full config object. - Updated: The Changes tab now renders raw
configuration.yamland file-write history as a line-highlighted text diff, instead of YAML-quoting the file contents. - Updated: Better MCP tool descriptions so agents can pick the right tool on the first try.
- Updated:
get_stateandget_statesnow return a compact, domain-aware view by default (key attributes only) to cut token cost. - Updated:
search_entitiesnow ranks results by relevance and supports multi-word queries. - Tests: Hardening
- Docs: Documented
get_yaml_configreturnsconfiguration.yamlverbatim (inline secrets included; prefer!secretreferences), and that a saved dashboard layout carries the same indirect-control caveat as authored automations. - Docs: Explained the authoring indirect-control boundary more precisely: granting
cap_automation_writeorcap_script_writeis broad HA write access, because a stored automation's actions run under Home Assistant natively and are not re-gated by ATM capabilities or MESA when it fires.
See the v2.0.0 release notes.
Report issues at github.com/sfox38/ATM/issues.