BETA — Open to testers. Tell us what to fix on @vomehome or via a tester code.

Agent API & MCP — Security Review

VomeHome lets an AI coding agent read and control your Home Assistant. That is a genuinely sensitive capability, so this page documents how it is constrained, what we deliberately do not allow, and the limitations we currently accept. It is written to be useful to someone deciding whether to trust it — which means the known weaknesses are listed alongside the controls.

For the wider platform, see Security & Privacy. Findings and fixes below are dated so you can tell how current this is.

The design in one sentence

The agent never receives a Home Assistant credential. It gets a revocable VomeHome API token, and every Home Assistant read or write is brokered through our API where the policy is enforced server-side.

This is the property everything else rests on. A compromised or misbehaving agent cannot bypass the broker to reach Home Assistant directly, because it was never given anything that would let it — the Home Assistant token stays on our server and is never returned to a caller. A read-only token genuinely cannot change anything; that is not a client-side setting an agent could ignore.

Authorisation & scopes

  • Reads by default, writes by grant. A token is read-only unless you explicitly grant more. Service calls require ha:write; rewriting automations, scripts and scenes requires the separate ha:config; creating instances requires the account-wide instances:write.
  • Write permissions are per instance, not per account. A token granted write access to one Home Assistant cannot write to another, even though both belong to you.
  • Reads are per instance too. An instance you do not grant is invisible to the token — not merely read-only. A key issued for one Home Assistant cannot enumerate the states, automations or logs of another. Granting control or edit-automations implies read for that same instance, because an agent has to see state before acting on it.
  • Ownership is re-checked on every call, not cached from when the token was minted.
  • Sensitive domains are refused even with write access. Locks, alarm panels, covers, climate, vacuums, valves, water heaters, mowers and cameras are blocked server-side. A generic call such as homeassistant.turn_on is checked against the domain of every target entity, so it cannot be used to reach a blocked domain indirectly.
  • Flashing firmware requires ha:config, the same grant as rewriting automations. An agent can build and install firmware on your ESPHome devices over the relay — a genuinely physical capability, so it is gated on the edit permission rather than the control one, and every build is audited like any other write. Reading a device's logs or validating its configuration changes nothing and needs only read access. The shared secrets.yaml is refused outright, on both read and write.
  • Revocation is immediate and self-service, from the API tokens page.

Credential handling

  • Tokens are stored only as a SHA-256 hash. The plaintext is shown once, at creation, and cannot be recovered afterwards — not by you, and not by us.
  • 256 bits of entropy from a cryptographically secure generator, so the hash need not be slow to be safe against guessing.
  • Credentials are never written to logs, and never used as a rate-limit or cache key in raw form — where a token identifies a request internally, it is hashed first.

Multi-tenant session isolation

The hosted endpoint serves many customers from one process, so isolation between sessions is a first-order concern.

  • One session, one token. Each connection builds its own isolated context — its own clients and its own view of which instances exist. There is no shared mutable state between sessions.
  • Sessions are bound to the token that opened them. Presenting a valid session identifier with a different token is refused. The comparison is constant-time, so the binding cannot be probed by measuring response timings.
  • Session identifiers are cryptographically random (UUIDv4), not sequential or guessable.
  • Idle sessions are reaped after 30 minutes, rather than lingering until a client happens to disconnect.
  • The instances a session can see come from its own token, resolved at connection time — not from a shared list.

Auditability

  • Every brokered call is logged — allowed and denied, read and write. A refusal is as interesting as a success when you are reconstructing what an agent did.
  • Each entry records the user, the token used, the target instance, the method and path, the domain and service where applicable, the decision, the reason for a refusal, and the resulting status code.
  • Auditing never breaks the request it records. A logging failure is swallowed and reported internally rather than failing the call — a deliberate trade-off favouring availability, with the consequence noted under Limitations.

Transport & host controls

  • TLS in public, loopback in private. The endpoint is served over HTTPS with HSTS; the process itself binds only to loopback and is never directly reachable from the internet.
  • Brokered calls stay on the host. Internal API traffic travels over the loopback interface rather than out to the public internet and back.
  • Reduced-privilege service. The process runs as an unprivileged user with NoNewPrivileges, a private /tmp, a read-only view of home directories, and a strict read-only system view.
  • Request bodies are capped before parsing, so an oversized payload is rejected rather than buffered.
  • Per-token request quotas (see Finding 1 below).

Review findings — September 2026

Both issues below were found in our own review of newly written code, before release and before any customer could reach them. Neither was ever live. They are published for the same reason as the ones below: a security page that lists only successes is not evidence of much.

Finding 4 Device credentials were readable with read-only access Resolved

Severity: moderate — over-broad credential exposure within a single account. No cross-account exposure, and never released.

Work to let an agent edit ESPHome device configurations treated every file in that folder alike. One of them, secrets.yaml, is not a device configuration at all: it holds what every device authenticates with — the wifi password, the API encryption keys, the firmware-update passwords. A token holding nothing but read access could therefore have retrieved the credentials for a whole fleet of devices, and one with edit access could have replaced them. Device configurations refer to secrets by name and never by value, so nothing legitimate needed to read them. That file is now refused on both read and write, in the portal and again on the component running in your home.

Finding 5 A build's identifier was treated as authority on its own Resolved

Severity: moderate — would have allowed limited cross-account exposure of build output. Never released.

Firmware builds run for minutes, so they are tracked as jobs the caller polls for output. Ownership of the Home Assistant instance was checked on every poll, but the job identifier itself was then accepted unqualified — so someone who came by another user's job identifier could have read that build's output, or cancelled the installation, through an instance of their own. The identifiers are random and unguessable, but that is obscurity rather than access control, and they are handed to callers and recorded in audit trails. The owning instance now travels with every poll and cancellation, and a mismatch is indistinguishable from a job that does not exist.

Review findings — August 2026

All three issues below were found during the review of the hosted endpoint, before it had any real usage, and all three are fixed. They are published because a security page that lists only successes is not evidence of much.

Finding 1 Shared request quota across hosted users Resolved

Severity: low — availability only. No impact on confidentiality or integrity; permissions and audit logging were unaffected throughout.

Request quotas were enforced per client network address. When the MCP server moved from running on each user's own machine to being hosted by us, every user's brokered calls began arriving from a single address and therefore shared one quota — so a single busy agent could have exhausted it for other users. Quotas for token-authenticated internal traffic are now keyed on the token itself, giving each user their own. Public traffic is still keyed per address, so the change cannot be used to escape a quota.

Finding 2 Deployment could silently drop web-server routes Resolved

Severity: low — availability only, no exposure of data or bypass of access control.

Our deployment regenerates the web-server configuration from a template. Any route added by hand outside that template was therefore removed by the next deployment. This affected the new agent endpoint and, more importantly, revealed a pre-existing latent fault in the off-site backup upload route, which had the same weakness and would have been dropped by any deployment. Both routes are now produced by the template itself, so they cannot drift out again.

Finding 3 Read access was account-wide, not per instance Resolved

Severity: moderate — over-broad read access within a single account. No cross-account exposure: a token could only ever reach Home Assistant instances belonging to its own owner, and write protection was never affected.

Ticking instances when creating a key granted write access to those instances, but read access was a single account-wide permission. Every key could therefore read every Home Assistant on its owner's account — states, entities, automations and logs — including instances deliberately left unticked. Two things combined to cause it: reads were checked account-wide rather than per instance, and a key created with no explicit account-wide permissions fell back to a default that included the read wildcard. Reads are now granted and enforced per instance, and an ungranted instance is invisible to the key rather than readable. Reported by a customer who noticed an agent reading an instance they had not given it.

Known limitations

Current, accepted trade-offs. We would rather state them than have you discover them.

  • Tokens issued before August 2026 can still read everything. They hold an account-wide read scope, which is honoured as an all-instances wildcard so they keep working. Only newly issued tokens get per-instance read gating. If you minted a key before this change and want it limited to specific instances, re-save its permissions on the API tokens page, or revoke and re-issue it.
  • Authentication is a bearer token, not OAuth. The Model Context Protocol specification expects remote servers to implement OAuth 2.1. We do not yet. In practice this means your token sits in a configuration file on your own machine, in plain text, with the same handling requirements as any other API key — protect the file, and revoke the token if you suspect exposure. Adding OAuth is on the roadmap; it requires an authorisation server and a consent flow for choosing which instances a client may reach, so it is a substantial piece of work rather than a quick change.
  • A bad firmware build can take a device offline. Installing firmware is a physical act, and a device that accepts a broken build may need to be reached with a cable to recover. ESPHome's own safe-mode and bootloader rollback usually prevent that, and the build has to succeed before anything is installed, but the risk is not zero. This is why installing is gated on the edit permission rather than the control one: a token that can only call services cannot flash anything.
  • Device logs contain your wireless network name. An ESPHome device prints its wifi SSID and access-point address in its own startup log. The dashboard hides them visually, but that is a display effect, not redaction — anything reading the log stream, including an agent with read access, sees them. They are not credentials, and the password is never printed, but a read-only token does learn which network the device is on.
  • Tokens do not expire by default. They remain valid until you revoke them. Set an expiry when creating one if that suits your use better.
  • Session state is held in memory. A restart of the service ends open sessions; clients reconnect automatically. It also means the endpoint runs as a single process, which is a capacity consideration rather than a security one.
  • The sensitive-domain list is a block list. Anything not on it is permitted once write access is granted. A newly invented integration category would not be blocked until the list is updated.
  • Audit logging is best-effort. It is designed never to break the request it records, so a database failure could in principle lose an entry rather than refuse the call.
  • An agent with write access can act. No amount of scoping changes the fact that granting ha:write lets an AI agent turn things on and off in your home. Grant the narrowest scope that does the job.

Reporting a problem

If you find a security issue in the agent API or MCP endpoint, please email security@vome.io. We will investigate every legitimate report and ask for reasonable time to fix before public disclosure. The MCP server itself is open source and can be read at github.com/Vortitron/home-assistant-mcp.

Reviewed August 2026, covering the hosted MCP endpoint and the brokered agent API. Updated as controls change.