Why give your AI agent basically unrestricted access to your home? ATM is a drop-in replacement for Home Assistant's native MCP server. Security focused, ATM implements all 20 native HA MCP tools so your existing AI client setup works without changes.
The big difference is control and security: each client gets its own token scoped to exactly the entities you allow, with its own rate limit and optional expiry. Every request is logged. If a token is ever compromised, one click revokes it and terminates all open connections immediately.
ATM runs entirely inside Home Assistant. No extra process, no cloud dependency, no configuration beyond the ATM panel.
| LLAT + native MCP | ATM token | |
|---|---|---|
| MCP tool compatibility | 20 native tools | Same 20 tools, identical names and responses; plus 16 additional tools |
| MCP Prompts and Resources | Native HA behavior | Identical for pass-through tokens; permission-scoped for scoped tokens |
| Client reconfiguration needed | /api/mcp | /api/atm/mcp (URL change only) |
| Entity filtering | Binary: expose/hide, same for all clients | Four permission states, per token |
| Per-client control | No - all clients see the same exposed entities | Yes - each token has independent permissions |
| Read-only access | No | Yes - YELLOW state allows reads, blocks writes |
| Audit trail | None | Every request logged with outcome and entity |
| Rate limiting | None | Per-token, configurable |
| Expiry | None | Optional, auto-archived on expiry |
| Revocation | Revoke LLAT via HA profile page | Instant, terminates open connections immediately |
| Sensitive attribute scrubbing | None | Always applied |
If you are connecting Claude Code, Cursor, ChatGPT, Antigravity, or any other AI tool to your Home Assistant, ATM gives you control that the native system cannot provide.
Getting started
- Requirements
- Installation
- Connecting Claude Code via MCP
- Available MCP Tools
- Tools Reference
- Using Third-Party MCP Servers
Reference
- The Permissions Panel
- The Permission System
- Capability Flags
- Pass-Through Mode
- Rate Limiting
- Security
- Telemetry and Sensors
- Global Settings
- Audit Log
- HA Events
- Route Reference
- Home Assistant 2024.5 or later
- In HACS, go to Integrations and click the menu in the top-right corner.
- Choose Custom repositories.
- Enter
https://github.com/sfox38/atmand select Integration as the category. - Click Add, then find ATM in the HACS integration list and install it.
- Restart Home Assistant.
- Copy the
custom_components/atmfolder into your HA config directory undercustom_components/atm. - Restart Home Assistant.
After installation, go to Settings > Devices and services > Add integration and search for "Advanced Token Management". Click through the single-step config flow. Only one ATM instance can be configured at a time.
Note
Once installed, open the ATM panel in your Home Assistant sidebar to manage your tokens.
Tip
Migrating from the native HA MCP server? ATM replaces it entirely. Once you have created a token and updated your AI client's MCP URL to point at /api/atm/mcp, you can disable or remove the native HA MCP integration from Settings > Devices and services. Your AI client configuration needs only a URL change - the tool names and parameters are identical.
ATM exposes an MCP endpoint at /api/atm/mcp. This is the recommended way to connect Claude Code to your Home Assistant instance.
In the ATM sidebar panel, go to the Tokens tab and click Create Token. Give it a name like claude-code and click Create. Copy the token value when it appears. It is shown exactly once and cannot be retrieved later.
A new token has no permissions by default. Use the permissions panel to grant access to the domains, devices, and entities you want Claude to work with. See The Permissions Panel for a walkthrough of how the tree and permission buttons work.
You can also enable capability flags to unlock specific operations such as restarting HA or reading system logs. See Capability Flags for the full list.
Run this command in your terminal, replacing the URL with your HA address and the token you copied:
claude mcp add --transport http home-assistant \
http://your-ha-address:8123/api/atm/mcp \
--header "Authorization: Bearer atm_your_token_here"If you use Nabu Casa remote access or a custom domain:
claude mcp add --transport http home-assistant \
https://your-instance.ui.nabu.casa/api/atm/mcp \
--header "Authorization: Bearer atm_your_token_here"Start a new Claude Code session and run /mcp. The home-assistant server should appear as connected. Ask Claude to list your entities or check a light state to confirm it is working.
ATM implements all 20 native HA MCP tools using the same tool names and response formats, and exposes the same MCP Prompts and Resources as the native HA MCP server. It also adds ATM-specific tools for direct entity access and system operations. All tools, prompts, and resources are scoped to the token's Permissions Tree. Pass-through tokens receive the same prompt and resource content as the native HA MCP server.
Native HA MCP tools - functionally identical to the native HA MCP server:
| Tool | Description |
|---|---|
GetLiveContext |
YAML snapshot of accessible entity states |
GetDateTime |
Current date, time, and timezone |
HassTurnOn / HassTurnOff |
Turn devices on or off by area, name, floor, or domain |
HassLightSet |
Set light brightness, color, or color temperature |
HassFanSetSpeed |
Set fan speed |
HassClimateSetTemperature |
Set climate device target temperature |
HassSetPosition |
Set position of covers or similar devices |
HassSetVolume / HassSetVolumeRelative |
Set or adjust media player volume |
HassMediaPause / HassMediaUnpause |
Pause or resume media playback |
HassMediaNext / HassMediaPrevious |
Skip tracks on a media player |
HassMediaSearchAndPlay |
Search and play media |
HassMediaPlayerMute / HassMediaPlayerUnmute |
Mute or unmute a media player |
HassCancelAllTimers |
Cancel all timers in an area |
HassStopMoving |
Stop a moving cover or device |
ATM entity tools - direct entity access also filtered by the Permissions Tree:
| Tool | Description |
|---|---|
get_state |
Current state of one entity |
get_states |
All accessible entity states |
get_history |
State history (supports 24h, 7d, 2w, 1m) |
get_statistics |
Long-term statistics for numeric entities |
call_service |
Call any HA service by domain and name |
System tools - gated by capability flags:
| Tool | Requires flag |
|---|---|
render_template |
allow_template_render |
get_config |
allow_config_read |
restart_ha |
allow_restart |
get_logs |
allow_log_read |
HassBroadcast - announce a message via assist satellite devices |
allow_broadcast |
create_automation / edit_automation / delete_automation |
allow_automation_write |
create_script / edit_script / delete_script |
allow_script_write |
MCP Prompts - same prompt protocol as the native HA MCP server:
| Prompt | Description |
|---|---|
Default prompt for Home Assistant {name} |
HA instance system prompt. Pass-through tokens receive the same prompt as the native HA MCP server. Scoped tokens receive a permission-filtered version. |
MCP Resources - readable via resources/read:
| URI | Description |
|---|---|
homeassistant://assist/context-snapshot |
Current entity state snapshot. Same URI as the native HA MCP server; content is scoped to the token's permissions. |
atm://server-info |
ATM server metadata, token info, and version. JSON format. |
Claude can only see and act on entities within the token's permission scope.
Note
After enabling or disabling a capability flag, your MCP client must reconnect to receive the updated tool list. In Claude Code, use the /mcp menu and select Reconnect.
Returns the current state of a single entity. Requires READ or WRITE permission on the entity.
Parameters:
entity_id(string, required) - Entity ID (e.g.,light.living_room)
Returns: Object with state, attributes, and last_changed timestamp. Sensitive attributes are always scrubbed.
Returns all entity states accessible to the token. Filtered by the Permissions Tree - only entities with READ or WRITE permission are included.
Parameters: None
Returns: Array of state objects. Pass-through tokens receive all non-ATM entities.
Returns historical state changes for an entity. Supports relative time strings: 24h (24 hours ago), 7d (7 days ago), 2w (2 weeks ago), 1m (30 days ago). Max range is 7 days.
Parameters:
entity_id(string, required) - Entity IDstart_time(string, optional) - ISO timestamp or relative string. Defaults to 24 hours ago.end_time(string, optional) - ISO timestamp or relative string. Defaults to now.
Returns: Array of history entries with state and timestamp. If the range exceeds 7 days, it is silently clamped. Actual queried range is returned in X-ATM-History-Start and X-ATM-History-End response headers.
Returns long-term statistics for numeric entities. Supports hourly, daily, weekly, monthly, or 5-minute aggregation.
Parameters:
entity_id(string, required) - Entity IDstart_time(string, optional) - ISO timestamp or relative stringend_time(string, optional) - ISO timestamp or relative stringperiod(string, optional) - One of5minute,hour,day,week,month. Defaults tohour.
Returns: Statistics array with min, max, mean, sum, and state values for each period.
YAML snapshot of all accessible entity states in a format optimized for LLM context. Equivalent to the native HA MCP GetLiveContext tool but filtered by the token's permissions.
Parameters: None
Returns: YAML-formatted string with entity states. Pass-through tokens receive the same output as the native HA MCP server.
Returns the current date, time, and timezone. Does not require any special permissions.
Parameters: None
Returns: Object with date (YYYY-MM-DD), time (HH:MM:SS), and timezone.
Call any HA service. Requires appropriate permission for the target entities.
Parameters:
service(string, required) - Service name indomain/serviceformat (e.g.,light/turn_on)entity_id(array, optional) - Explicit entity IDsdevice_id(array, optional) - Device IDs (expanded to entity list internally)area_id(array, optional) - Area IDs (expanded to entity list internally)data(object, optional) - Service parameters
Behavior:
device_idandarea_idare expanded to explicit entity lists before calling HA. Denied entities are silently excluded.- If all entities in the call resolve to denied, returns 403.
- Service responses are scanned for entity IDs. Any inaccessible ID is replaced with
<redacted>. - Physical control services (lock, alarm, cover mutation) require
allow_physical_controlflag even with WRITE permission. - Restart and stop services require
allow_restartflag.
Returns: Service response data (if the service declares a response schema). Some services return nothing.
Read HA configuration data. Requires allow_config_read flag.
Parameters: None
Returns: HA configuration object including integrations, packages, automation, and scripting settings.
Read recent HA system log entries. Requires allow_log_read flag. ATM's own log entries are always excluded. Token values are scrubbed from messages and tracebacks.
Parameters:
limit(integer, optional) - Number of entries to return. Defaults to 50. Max 100.level(string, optional) - Minimum log level. One ofINFO,WARNING,ERROR. Defaults toWARNING.
Returns: Array of log entries with timestamp, level, logger, and message.
Render a Jinja2 template with access to HA state. Requires allow_template_render flag. The template environment is permission-scoped - templates can only access entities the token has READ or WRITE permission for.
Parameters:
template(string, required) - Jinja2 template string
Returns: Rendered template result as a string.
Create a new automation in automations.yaml. Requires allow_automation_write flag. This tool does NOT consult the Permissions Tree - it writes to YAML directly.
Parameters:
alias(string, required) - Automation friendly nametrigger(array, required) - Trigger array in HA automation formataction(array, required) - Action array in HA automation formatcondition(array, optional) - Condition arraymode(string, optional) - One ofsingle,restart,queued,parallel. Defaults tosingle.
Returns: Created automation config with assigned ID.
Security note: See Automation and script write flags.
Edit an existing automation. Requires allow_automation_write flag and valid automation ID.
Parameters:
automation_id(string, required) - Automation ID (the slug)- All parameters from
create_automation(replaces the entire config)
Returns: Updated automation config.
Delete an automation. Requires allow_automation_write flag.
Parameters:
automation_id(string, required) - Automation ID to delete
Returns: Confirmation message.
Create a new script in scripts.yaml. Requires allow_script_write flag. This tool does NOT consult the Permissions Tree.
Parameters:
script_id(string, required) - Script slug (lowercase alphanumeric and underscore only)alias(string, required) - Script friendly namesequence(array, required) - Sequence of actions in HA script formatmode(string, optional) - One ofsingle,restart,queued,parallel. Defaults tosingle.variables(object, optional) - Script-level variablesfields(object, optional) - Input field definitions for callable scripts
Returns: Created script config.
Security note: See Automation and script write flags.
Edit an existing script. Requires allow_script_write flag and valid script ID.
Parameters:
script_id(string, required) - Script ID (the slug)- All parameters from
create_script(replaces the entire config)
Returns: Updated script config.
Delete a script. Requires allow_script_write flag.
Parameters:
script_id(string, required) - Script ID to delete
Returns: Confirmation message.
Restart Home Assistant. Requires allow_restart flag. This is a pass-through-exempt capability - even pass-through tokens must have this flag enabled.
Parameters: None
Returns: Confirmation that restart has been queued.
The following tools are functionally identical to the native HA MCP server. They use the same tool names, parameters, and response formats. All are scoped to the token's Permissions Tree.
Turn entities on or off by area, name, floor, or domain.
Parameters:
area(string, optional) - Area namefloor(string, optional) - Floor namename(string, optional) - Entity friendly namedomain(array, optional) - Domain(s)device_class(array, optional) - Device class(es)
Behavior: Returns action_done with a list of successfully controlled entities and a list of failed entities. Only entities with WRITE permission are included. If no accessible entities match, returns "No accessible entities matched."
Set brightness, color, or color temperature of accessible lights.
Parameters:
area(string, optional) - Area namefloor(string, optional) - Floor namename(string, optional) - Light friendly namebrightness(integer 0-100, optional) - Brightness percentagecolor(string, optional) - CSS color name or hextemperature(integer, optional) - Color temperature in kelvin
Set fan speed by percentage.
Parameters:
area(string, optional)floor(string, optional)name(string, optional)percentage(integer 0-100, required) - Fan speed percentage
Set climate device target temperature.
Parameters:
area(string, optional)floor(string, optional)name(string, optional)temperature(number, required) - Target temperature
Set position of covers, blinds, or similar devices (0-100).
Parameters:
area(string, optional)floor(string, optional)name(string, optional)position(integer 0-100, required) - Position percentage
Set or adjust media player volume.
HassSetVolume Parameters:
area(string, optional)floor(string, optional)name(string, optional)volume_level(integer 0-100, required) - Absolute volume
HassSetVolumeRelative Parameters:
volume_step(string or integer, required) - One of"up","down", or an integer percentage change (-100 to 100)
Pause or resume media playback.
Parameters:
area(string, optional)floor(string, optional)name(string, optional)
Skip to next or previous track.
Parameters:
area(string, optional)floor(string, optional)name(string, optional)
Search and play media on a player.
Parameters:
search_query(string, required) - What to search formedia_class(string, optional) - Media type (album, artist, track, playlist, etc.)area(string, optional)floor(string, optional)name(string, optional)
Mute or unmute a media player.
Parameters:
area(string, optional)floor(string, optional)name(string, optional)
Cancel all running timers in an area.
Parameters:
area(string, optional) - Area name. If omitted, cancels timers in all areas.
Returns: action_done with speech_slots: { "canceled": N } where N is the count of canceled timers.
Stop a moving cover or similar device.
Parameters:
area(string, optional)floor(string, optional)name(string, optional)
Send an announcement through Assist satellite devices. Requires allow_broadcast flag.
Parameters:
message(string, required) - Message to announce
Returns: action_done on success.
Third-party MCP servers such as ha-mcp run as standalone processes and make calls directly to HA's native REST API (/api/). HA's authentication middleware only accepts Long-Lived Access Tokens - it has no knowledge of ATM tokens. As a result, ATM tokens cannot be used as a drop-in replacement for an LLAT with these servers.
If you need scoped, audited, revocable access for an AI client, point it at ATM's own MCP endpoint (/api/atm/mcp) instead. ATM's 20 native HA MCP tools cover the same everyday operations and apply your Permissions Tree on every call. For clients that specifically require a third-party server's extended tool set with no access restrictions, use a LLAT directly.
When you open a token in the ATM panel, the token detail page shows the Permissions Tree on the right side of the screen. Domains sit at the top level, devices nest underneath, and individual entities live at the leaves. You expand a domain to see its devices, expand a device to see its entities.
Each row in the tree has four colored buttons. Click one to set the permission state for that node:
- ⬜ GREY - no opinion. The node inherits its permission from its parent. This is the default for every node.
- 🟡 YELLOW - read-only. The token can read this entity's state but cannot call services that change it.
- 🟢 GREEN - read and write. The token can read state and call services.
- 🔴 RED - hard deny. Blocks this node and every entity underneath it, no matter what any other node says.
You do not have to set every node individually. The typical pattern is to set a whole domain or device to 🟡 YELLOW or 🟢 GREEN and leave everything below it at ⬜ GREY - child nodes will inherit the parent's color automatically. Then use 🔴 RED on specific devices or entities to carve out exceptions.
After granting permission to an entity, you can add an optional hint. Hints are surfaced to LLMs via the context endpoint to help them understand what an entity represents - e.g., "This lamp is on Rachel's desk, not the ceiling light".
The Select by Area button at the top of the Permissions Tree card lets you bulk-apply a permission state to all entities in a HA area at once. Pick the area, pick the state (READ, WRITE, DENY, or remove grant), and ATM sets every entity in that area in one step. Useful for quickly scoping a token to a room without clicking through the tree manually.
The Effective Permission Emulator shows you what ATM will actually decide for any entity. Type an entity ID (or click one in the tree or Permission Summary) and ATM runs the full two-pass resolver and shows you the result: WRITE, READ, or no access, which node in the ancestor chain determined the outcome, and the hint text if one is set.
This is the fastest way to verify your tree is doing what you intended. If the result surprises you, the path output tells you exactly which ancestor is overriding it.
Below the Effective Permission Emulator is the Permission Summary - a compact table listing every node in the Permissions Tree that has been explicitly set to something other than ⬜ GREY. It shows the node type (Domain, Device, or Entity), the friendly name, the ID, and the current state.
You can sort the table by any column. Clicking an entity row in the Permission Summary populates the Effective Permission Emulator with that entity, just like clicking it in the tree.
If the table is empty, no explicit grants have been set and the token has no access to anything.
Read all lights, but only control the living room:
Set the light domain to 🟡 YELLOW. Set light.living_room to 🟢 GREEN. Every light is readable; only the living room light is writable.
Full access except the guest bedroom: Set a domain to 🟢 GREEN. Set a guest bedroom device to 🔴 RED. Every entity on that device becomes completely inaccessible, regardless of the domain setting.
Block one noisy sensor inside an otherwise permitted device: Set the device to 🟢 GREEN. Set just that sensor entity to 🔴 RED.
For a detailed explanation of how ATM resolves permissions under the hood, see The Permission System.
Every token has a Permissions Tree organized into three levels: domains, devices, and entities. Each node carries one of four states.
⬜ GREY - no opinion, inherit from parent. A ⬜ GREY entity under a 🟢 GREEN domain gets the domain's permission. ⬜ GREY at every level means no access.
🟡 YELLOW - read-only. The token can read the current state. It cannot call services that change state.
🟢 GREEN - read and write. The token can read state and call services.
🔴 RED - explicit deny. Blocks this node and everything beneath it. 🔴 RED cannot be overridden by any child node. It is a hard stop.
When a request arrives for an entity, ATM runs two checks:
Pass 1 - 🔴 RED scan. ATM walks the ancestor chain: entity, then device, then domain. If any node is 🔴 RED, the request is denied immediately. No other node matters.
Pass 2 - most specific grant. ATM walks the same chain looking for the most specific non-⬜ GREY node. That color becomes the effective permission. If no non-⬜ GREY node exists, the result is NO_ACCESS, which is indistinguishable from DENY to the caller.
Token with no grants. All ⬜ GREY. The token can see and do nothing. This is the safe default. Add grants incrementally.
Read all lights, control the living room.
- Set the
lightdomain to 🟡 YELLOW. - Set
light.living_roomto 🟢 GREEN.
The token can read any light state. It can only call turn_on/turn_off on light.living_room.
Full access to lights except the guest bedroom.
- Set the
lightdomain to 🟢 GREEN. - Set the guest bedroom device to 🔴 RED.
All lights are writable. Every entity on the guest bedroom device is denied, even though the domain is 🟢 GREEN. 🔴 RED wins.
Block one diagnostic sensor inside a permitted device.
- Set the device to 🟢 GREEN.
- Set the specific diagnostic entity to 🔴 RED.
All entities on the device are writable except that one sensor, which is completely inaccessible.
Three domains carry a specific risk that is easy to overlook: automation, script, and scene.
Granting WRITE (🟢 GREEN) to entities in these domains allows a client to:
- Trigger an automation (
automation.trigger) - the automation runs under Home Assistant's full context, not ATM's - Run a script (
script.turn_on) - same - Activate a scene (
scene.turn_on) - applies a preset that can set state on any entity in the scene
ATM checks the token's permission for the automation, script, or scene entity itself. But the downstream effects - the lights that get turned on, the locks that get toggled, the climate changes - happen entirely outside ATM's scope. A token with NO_ACCESS to a door, but WRITE on automation.* can still unlock a door if a triggered automation does it.
Granting READ (🟡 YELLOW) to these domains is safe. It allows reading whether an automation is enabled or a scene exists, without the ability to trigger anything.
The Permissions Tree marks automation, script, and scene with a [!] badge as a reminder of this risk.
These apply to every token including pass-through tokens and are not configurable:
- The
atmdomain (all internal ATM sensors) is permanently blocked and invisible. - Entity attributes that could expose security credentials (
access_token,entity_picture,stream_url,still_image_url) are stripped from every state response.
Some operations require explicit opt-in even for tokens with 🟢 GREEN domain access:
| Flag | What it enables | Pass-through exempt |
|---|---|---|
allow_restart |
homeassistant.restart and homeassistant.stop |
yes |
allow_physical_control |
Lock, alarm, and cover mutation services (e.g. lock.unlock, alarm_control_panel.alarm_disarm, cover.open_cover) |
yes |
allow_automation_write |
Creating, editing, and deleting automations via the MCP tools. See security note below. | yes |
allow_script_write |
Creating, editing, and deleting scripts via the MCP tools. See security note below. | yes |
allow_config_read |
Reading HA configuration data and the event bus listener list | no |
allow_template_render |
Rendering Jinja2 templates (permission-scoped environment) | no |
allow_service_response |
Return response data from services that support it (e.g. conversation.process). Silently omitted for services that do not declare a response schema. |
no |
allow_broadcast |
Sending announcements via the HassBroadcast MCP tool through assist satellite devices |
no |
allow_log_read |
Reading HA system log entries via the get_logs MCP tool and GET /api/atm/logs. Logs may contain IP addresses and operational details. ATM's own entries are always excluded and token values are scrubbed from messages and tracebacks. |
yes |
The five pass-through-exempt flags (allow_restart, allow_physical_control, allow_automation_write, allow_script_write, allow_log_read) must be explicitly enabled even for pass-through tokens. All other flags are bypassed by pass-through tokens.
allow_automation_write and allow_script_write are elevated-trust capabilities. Enable them only for tokens held by clients you fully trust and control.
These flags are all-or-nothing. The automation and script write tools (create_automation, edit_automation, delete_automation, create_script, edit_script, delete_script) write directly to automations.yaml and scripts.yaml. They do not consult the token's Permissions Tree. A client with allow_automation_write enabled can write an automation referencing any entity in Home Assistant, regardless of what the token is permitted to access directly via get_state or call_service.
The Permissions Tree cannot restrict automation/script write. Setting the automation or script domain to READ or DENY in the Permissions Tree has no effect on these MCP tools. A DENY on automation.* only blocks entity-scoped operations (reading automation entity state, calling automation.trigger). It does not prevent the write tools from creating or modifying automation YAML.
Triggered actions run outside ATM. An automation or script created through ATM is triggered by HA's own automation engine, which runs under HA's own context, not ATM's. Permission checks do not apply to the actions taken when a triggered automation runs.
In practice, a token with a narrow entity scope but allow_automation_write enabled could - through a crafted automation - indirectly control entities it cannot access directly. Only enable these flags for clients you would trust with broad HA access.
Pass-through tokens bypass the three-level permission check and have 🟢 GREEN access to all entities. They are intended for trusted tools where managing a full Permissions Tree is impractical or unnecessary.
Pass-through does NOT bypass:
- The
atmdomain blocklist - Sensitive attribute scrubbing
- Rate limiting
allow_restart- callinghomeassistant.restartorhomeassistant.stopstill requires this flag.allow_physical_control- lock, alarm, and cover mutation services still require this flag.allow_automation_write- creating, editing, or deleting automations still requires this flag.allow_script_write- creating, editing, or deleting scripts still requires this flag.allow_log_read- reading HA system log entries still requires this flag.
These five flags must always be explicitly enabled regardless of pass-through mode. All other capability flags (allow_config_read, allow_template_render, allow_service_response, allow_broadcast) are bypassed.
The ATM panel shows a confirmation dialog before enabling pass-through on a token. When using the admin API directly, the PATCH request must include "confirm_pass_through": true alongside "pass_through": true. Omitting it returns a 400 error. Use pass-through only for tools you fully control. For anything externally hosted or shared, use the scoped Permissions Tree instead.
Every token has a sliding window rate limit. The defaults are 60 requests per minute with a burst allowance of 10 per second. Both are configurable per token. Setting rate_limit_requests to 0 disables rate limiting entirely for that token.
When a request is rate limited, ATM returns HTTP 429 with a Retry-After header. Successful responses include rate limit headers:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1712345678
If notify_on_rate_limit is enabled in global settings, HA creates a persistent notification when a token hits its limit. This is throttled to once per token per minute.
- Tokens are 68 characters with a fixed
atm_prefix. Any value that does not match this exact format is rejected before any storage lookup. - Only the SHA-256 hash of the token is stored. The raw value is never written to disk or logs.
- Token comparisons always use a constant-time algorithm (
hmac.compare_digest). String equality (==) is never used for token validation. - Tokens are only accepted in the
Authorization: Bearerheader. Query parameters are rejected with 401. No token value ever appears in HA logs.
- Every request is validated against the full two-pass permission algorithm. No endpoint implements its own shortcut.
- Entity not found and entity inaccessible return identical response bodies. A caller cannot determine whether an entity exists or is simply blocked.
- Sensitive attributes are stripped from every state response, for every token type. See Sensitive attributes stripping.
- Service calls that include
device_idorarea_idare always expanded to an explicit entity list before being passed to HA. Denied entities are silently excluded. - If all entities in a service call resolve to denied, ATM returns 403 rather than calling HA with an empty list.
- Service response data is scanned for entity IDs. Any entity ID the token cannot access is replaced with
"<redacted>". - If an entity ID in a service call does not exist in the HA entity registry, ATM returns 403. Entity creation via service calls is not permitted.
- Physical control services (
lock.unlock,alarm_control_panel.alarm_disarm,cover.open_cover, and related services) requireallow_physical_controlin addition to entity-level WRITE permission. This applies even to pass-through tokens. - Automation and script write MCP tools bypass the Permissions Tree. Setting the
automationorscriptdomain to RED or YELLOW does not prevent these tools from writing YAML. See Automation and script write flags.
ATM removes four sensitive attributes from every state response, regardless of token type or permission level:
entity_picture- URLs to entity images and icons, often containing authentication tokens or private asset pathsstream_url- Direct stream URLs (e.g., from cameras), which may contain credentials or expose internal network topologyaccess_token- Authentication tokens embedded in entity state (e.g., from integrations that store temporary credentials)still_image_url- Static image URLs that might contain sensitive identifiers or auth parameters
Why all tokens, all the time:
- Even pass-through tokens (which bypass entity permissions) still get these attributes scrubbed. A pass-through token that can call any service doesn't need access to embedded credentials; it already has the power to act.
- Even high-permission scoped tokens with WRITE access get these attributes scrubbed. Permission grants control what actions a token can take, not what secrets it can read.
- This prevents accidental credential leakage through state snapshots, audit logs, or third-party integrations that consume ATM responses.
Where stripping happens:
- Proxy view:
/api/atm/states,/api/atm/entities/{entity_id},/api/atm/history, etc. - MCP tools:
GetLiveContext,get_history,get_state, etc. - Service response data filtering: if a service returns entity state in its response (e.g., a script fetching entity details), those attributes are redacted before returning to the caller.
Rotation generates a new raw token value for an existing token while keeping all of its permissions, capability flags, rate limit settings, and audit history intact. The old value is invalidated the moment rotation is confirmed - there is no grace period. The new value is shown once and cannot be retrieved again. Use rotation when you suspect a token value has been exposed but do not want to rebuild the Permissions Tree from scratch.
Revocation permanently retires a token. When a token is revoked, ATM immediately archives it to storage, terminates all open SSE connections for that token, destroys its rate limiter state, removes its sensor entities, and fires an atm_token_revoked event. All of this happens before the revoke response is returned.
Expired tokens are treated identically to revoked tokens at validation time.
The admin API (/api/atm/admin/) requires a valid HA session and HA admin privileges. An ATM token cannot authenticate an admin request, even a pass-through token.
When the kill switch is enabled at startup, ATM registers no proxy or MCP routes. The endpoints do not exist - there is nothing to respond with 503. The admin panel remains accessible. Disabling the kill switch re-registers all routes immediately without an HA restart.
- Request bodies exceeding 1 MB are rejected with 413 before any processing.
- SSE connections are limited to 5 per token. A sixth connection is rejected with 429.
- History queries are capped at a 7-day time range. Requests spanning more than 7 days are silently clamped to the most recent 7 days. The actual queried range is returned in
X-ATM-History-StartandX-ATM-History-Endresponse headers. Passing astart_timeafterend_timereturns 400. - Every response includes an
X-ATM-Request-IDheader with a UUID that matches the corresponding audit log entry.
ATM creates six HA sensor entities for each active token. For a token named claude-code:
| Entity | Description |
|---|---|
sensor.atm_claude_code_status |
active, expired, or revoked |
sensor.atm_claude_code_request_count |
Total requests made with this token |
sensor.atm_claude_code_denied_count |
Requests blocked by permission rules |
sensor.atm_claude_code_rate_limit_hits |
Number of times this token has been rate limited |
sensor.atm_claude_code_last_access |
Timestamp of the most recent request |
sensor.atm_claude_code_expires_in |
Days until expiry, or No expiry if no expiry is set |
Sensors are removed automatically when a token is revoked. ATM sensors are blocked from all token access - external tools cannot read their own telemetry through ATM.
| Setting | Default | Description |
|---|---|---|
| Kill switch | Off | When on, proxy and MCP routes are unregistered entirely |
| Disable all logging | Off | Suppresses all auditing |
| Log allowed requests | On | Record successful requests |
| Log denied requests | On | Record blocked requests and unsupported MCP method calls |
| Log rate-limited requests | On | Record rate-limited requests |
| Log entity names | On | Include entity IDs in audit entries |
| Log client IP | On | Include caller IP in audit entries |
| Notify on rate limit | Off | Create a HA notification when a token is rate limited |
| Audit log flush interval | 15 min | How often to snapshot the in-memory log to disk. Set to "Never" to disable persistence entirely. |
| Maximum log entries | 10,000 | Capacity of the in-memory buffer and the on-disk snapshot. Reducing this trims the oldest entries immediately. |
ATM keeps a circular buffer of requests, queryable from the ATM panel or via the admin API. The default capacity is 10,000 entries, configurable in Global Settings. You can view the Audit Log in the AUDIT tab of the ATM panel - click on a row to get full information for an event.
Each entry records a unique request ID (matching the X-ATM-Request-ID response header), timestamp, token ID and name, HTTP method, resource path, outcome, and client IP.
Outcome values:
allowed- request succeeded.denied- blocked by ATM permission rules, blocklist, or a RED/NO_ACCESS result. Includes permission-based 404s.not_found- entity is genuinely absent from both HA state and the entity registry. From the caller's perspective it looks identical todenied, but the audit log distinguishes them so you can tell whether a token is hitting a missing entity or a permission wall.rate_limited- token exceeded its rate limit.not_implemented- the MCP client called a method ATM does not support (for example,resources/templates/list). This is a protocol-level gap, not a permission block, and does not increment the token's denied counter.invalid_request- request was structurally malformed and rejected before it reached permission checks, for example a template render call with a syntax error in the template body.
The audit log is stored in .storage/atm_audit.json and survives HA restarts. It is included in HA full backups and in partial backups of the .storage directory.
ATM flushes the in-memory buffer to disk on the configured interval (default: every 15 minutes), and also automatically on HA stop, integration reload, and integration unload. Set the interval to "Never" to keep the log in memory only and disable all disk writes.
| Event | Fired when |
|---|---|
atm_token_revoked |
A token is revoked |
atm_token_expired |
A token's expiry time passes and it is first accessed |
atm_token_rotated |
A token's raw value is rotated |
atm_rate_limited |
A token exceeds its rate limit (once per token per minute) |
Event data includes token_id, token_name, and timestamp. Revocation and rotation events also include revoked_by / rotated_by (the HA user ID of the admin who performed the action).
GET/POST /api/atm/admin/tokens List or create tokens
GET/PATCH /api/atm/admin/tokens/{id} Get or update a token
DELETE /api/atm/admin/tokens/{id} Revoke a token
GET/PUT /api/atm/admin/tokens/{id}/permissions Read or replace permissions tree
PATCH /api/atm/admin/tokens/{id}/permissions/domains/{node}
PATCH /api/atm/admin/tokens/{id}/permissions/devices/{node}
PATCH /api/atm/admin/tokens/{id}/permissions/entities/{node}
GET /api/atm/admin/tokens/{id}/resolve/{entity_id} Explain effective permission
POST /api/atm/admin/tokens/{id}/rotate Generate a new raw token value (old value immediately invalid)
GET /api/atm/admin/tokens/{id}/scope List all readable/writable entities
GET /api/atm/admin/tokens/{id}/stats Request counters
GET /api/atm/admin/tokens/{id}/audit Audit log for one token
GET /api/atm/admin/tokens/archived List archived tokens
DELETE /api/atm/admin/tokens/archived/{id} Delete an archived record
GET /api/atm/admin/entities Entity tree
GET /api/atm/admin/info Integration version info
GET /api/atm/admin/audit Global audit log
GET/PATCH /api/atm/admin/settings Global settings
DELETE /api/atm/admin/wipe Wipe all tokens and settings
GET /api/atm/states All accessible entity states
GET /api/atm/states/{entity_id} One entity state
POST /api/atm/services/{domain}/{service} Call a service
GET /api/atm/history/period/{timestamp} State history (max 7-day range)
GET /api/atm/statistics Long-term statistics
POST /api/atm/template Render a Jinja2 template
GET /api/atm/config HA configuration
GET /api/atm/events HA event bus listeners
GET /api/atm/services Accessible service list
GET /api/atm/logs Recent HA system log entries
GET /api/atm/mcp MCP SSE endpoint
POST /api/atm/mcp MCP Streamable HTTP endpoint
POST /api/atm/mcp/messages?session_id={id} MCP SSE message endpoint
GET /api/atm/mcp/context Token context summary
Report issues at https://github.com/sfox38/atm/issues.