Capabilities & Permissions
A widget runs sandboxed: it renders in an isolated shadow root, holds no Home Assistant connection or token, and cannot reach the network beyond Home Assistant and the Hub. To read or control anything in the home, it declares the access it needs in its manifest. The user approves that list when they install the widget, and the host enforces it on every call.
Access is deny-by-default: anything you don’t declare is blocked at runtime. So the first thing to get right when building a widget is its capability list.
This page is the developer’s view: how to declare and scope access. For the homeowner-facing explanation of the sandbox and why it’s safe, see Widget Security.
How access works
All Home Assistant interaction goes through the SDK’s data and service hooks. There is no other path: importing @glasshome/sync-layer directly fails the build, and fetch/WebSocket to Home Assistant is blocked. Each call carries your widget’s declared capabilities, and the host checks the call against them before touching Home Assistant.
- Reads (entity state, history, forecasts) require a
readgrant for the domain. - Service calls (turning a light on, setting a thermostat) require a
controlgrant for the domain. - A call that no grant authorizes is rejected, and the user is notified.
Declaring capabilities
- 1
List what the widget needs, minimally
Decide which Home Assistant domains the widget touches and whether it only shows them (
read) or also operates them (control). Ask for the least that makes the widget work. A widening update has to be re-approved by the user, and an over-broad request makes people hesitate to install. - 2
Add a capabilities array to the manifest
Each grant is
{ domain, access }, plus optional narrowing:// manifest.json "capabilities": [ { "domain": "light", "access": "control" }, { "domain": "sensor", "access": "read" } ]The same array goes in the inline
manifestyou pass todefineWidget. A widget that touches no Home Assistant data (a clock, say) declares an empty array:"capabilities": []Targeting SDK 1.x without a
capabilitiesarray is rejected at publish. A manifest may declare at most 32 grants. - 3
Use the data layer
Read and control through the SDK hooks. Reads need a matching
read(orcontrol) grant; service calls needcontrol:import { useEntity, useService } from "@glasshome/widget-sdk"; const light = useEntity(() => props.config.entityId); // needs read/control on "light" const { toggle } = useService(); // toggle(props.config.entityId) // needs control on "light"
Access levels
access | Grants | Use it for |
|---|---|---|
read | Reading state, attributes, history, forecasts for the domain | Widgets that only display |
control | Read and calling services on the domain | Widgets that operate devices |
control implies the ability to read the same domain, so you don’t need both for one domain.
Narrowing a grant
By default a grant covers a whole domain. Tighten it to specific entities or services when the widget only needs part of one.
To specific entities
entities is a list of entity-id patterns. The domain part stays literal; * globs the object id only, so a pattern can never widen a grant past its domain.
{ "domain": "light", "access": "control", "entities": ["light.living_*", "light.kitchen"] }
A grant scoped to specific entities cannot reach domain-wide services (a service call with no entity target). Leave entities off if the widget legitimately needs the whole domain.
To specific services
services restricts a control grant to named services. A call to any service outside the list is rejected.
{ "domain": "media_player", "access": "control", "services": ["media_play", "media_pause"] }
Both entities and services, when present, must list at least one entry.
What the user approves
The host renders each grant as a plain-language sentence on the install consent screen, generated from the exact same object the host enforces, so the prompt can never drift from what’s actually allowed:
| Grant | Shown to the user |
|---|---|
{ domain: "light", access: "control" } | Control your lights |
{ domain: "sensor", access: "read" } | Read your sensors |
{ domain: "light", access: "control", entities: ["light.living_*"] } | Control your lights (light.living_*) |
{ domain: "media_player", access: "control", services: ["media_play","media_pause"] } | Control your media players — only: media play, media pause |
The user approves that list and only that list.
Enforcement and updates
- Deny-by-default. Every read and service call is checked against your grants at runtime; anything unmatched is blocked and the user is notified. There is no way for widget code to opt out of the check.
- Widening re-prompts. If an update asks for more than the user approved (a new domain, broader access, removed narrowing), the host asks the user to approve again before the update can run. Narrowing or unchanged grants install silently.
- Verified on publish. The Hub validates your capability grammar and the bundle hash before a version goes live.