---
title: "Capabilities & Permissions"
description: "How a widget declares the Home Assistant access it needs. The capability grammar, access levels, narrowing, user consent, and host enforcement."
canonical: https://glasshome.app/docs/widgets/widget-capabilities
section: "Build Widgets"
updated: 2026-09-03
---
# Capabilities & Permissions

A widget runs sandboxed: an isolated shadow root, no Home Assistant connection or token, no 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 at install, and the host enforces it on every service call.

Control is **deny-by-default**: a service call you don't declare is blocked at runtime. Reads are shown to the user at install but not yet checked at runtime, so declare every domain you read: the consent screen is only as honest as your manifest, and read checks will block undeclared reads once they ship.

> **Info:** This is the developer's view. For the homeowner-facing explanation of the sandbox, see Widget Security.

## How access works

Home Assistant interaction goes only through the SDK's [data and service hooks](/docs/widgets/widget-sdk#home-assistant-data--services): importing `@glasshome/sync-layer` directly fails the build, and `fetch`/WebSocket to Home Assistant is blocked. The host checks every service call against your declared capabilities before touching Home Assistant.

**Reads** (entity state, history, forecasts) require a `read` grant for the domain; **service calls** require `control`.

## 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 works.

2. **Add a capabilities array to the manifest** Each grant is `{ domain, access }`, plus optional narrowing:

   ```jsonc
   // manifest.json
   "capabilities": [
     { "domain": "light", "access": "control" },
     { "domain": "sensor", "access": "read" }
   ]
   ```

   The same array goes in the inline `manifest` passed to `defineWidget`. A widget that touches no Home Assistant data (a clock, say) declares an empty array:

   ```jsonc
   "capabilities": []
   ```

   Targeting SDK 1.x **without** a `capabilities` array is rejected at publish. A manifest may declare at most 32 grants.

3. **Use the data layer** Read and control through the SDK hooks:

   ```tsx
   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 `read` on the same domain, so you never need both.

## Narrowing a grant

A grant covers a whole domain by default; tighten it when the widget needs only part of one.

### To specific entities

`entities` lists entity-id patterns. The domain part stays literal and `*` globs only the object id, so a pattern can never widen a grant past its domain.

```jsonc
{ "domain": "light", "access": "control", "entities": ["light.living_*", "light.kitchen"] }
```

An entity-scoped grant cannot reach **domain-wide** services (a service call with no entity target). Leave `entities` off if the widget needs the whole domain.

### To specific services

`services` restricts a `control` grant to named services; calls outside the list are rejected.

```jsonc
{ "domain": "media_player", "access": "control", "services": ["media_play", "media_pause"] }
```

`entities` and `services` must be non-empty when present. A service-narrowed grant covers its entities for its own domain's services only, so keep it off any grant an integration service has to reach through (see below).

## Integration services

Some integrations add services that act on **another domain's entities**. Music Assistant and Sonos own no entities of their own: their speakers appear as `media_player.*`, and services like `music_assistant.get_queue` or `sonos.snapshot` are aimed at those entities.

A call like that names two domains, and each needs a grant:

```jsonc
"capabilities": [
  { "domain": "media_player", "access": "control" },   // the speaker it acts on
  { "domain": "music_assistant", "access": "control" } // the service it calls
]
```

The host checks both: the service's own domain must be granted, and every entity the call targets must be covered by some grant. Granting `media_player` alone does not let a widget call Music Assistant services; granting `music_assistant` alone reaches no speaker.

Narrowing still applies. With `{ "domain": "media_player", "access": "control", "entities": ["media_player.kitchen"] }`, integration services reach the kitchen speaker and nothing else.

> **Info:** Services that take no entity at all (`music_assistant.get_library` takes a `config_entry_id`) act on the integration itself. Those need an un-narrowed grant on the integration's domain, the same rule as any other domain-wide service.

## What the user approves

The install consent screen renders each grant as a plain-language sentence, generated from the same object the host enforces, so the prompt cannot drift from what is 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 |

## Enforcement and updates

- **Deny-by-default for control.** Every service call is checked against your grants at runtime; anything unmatched is blocked and the user is notified. Widget code cannot opt out.
- **Reads are declared, checked later.** Read grants appear on the consent screen today; runtime read checks are coming, and will block any read your manifest does not declare.
- **Widening re-prompts.** An update asking for more than the user approved (a new domain, broader access, removed narrowing) must be re-approved before it runs. Narrowing or unchanged grants install silently.
- **Verified on publish.** The Hub validates your capability grammar and the bundle hash before a version goes live.

## See also

### [Data & service hooks](/docs/widgets/widget-sdk#home-assistant-data--services)

The SDK hooks every capability gates: useEntity, useService, and the rest.

### [Widget Security](/docs/dash/widget-security)

The homeowner-facing view of the sandbox and what it guarantees.