---
title: "Capabilities & Permissions"
description: "How a widget declares the Home Assistant access it needs. The capability grammar, access levels, narrowing to entities and services, what the user approves, and how the host enforces it."
canonical: https://glasshome.app/docs/widget-capabilities
section: "Widget Development"
updated: 2026-06-13
---
# 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.

> **Info:** 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 <a href="/docs/widget-security">Widget Security</a>.

## How access works

All Home Assistant interaction goes through the SDK's [data and service hooks](/docs/widget-sdk#home-assistant-data--services). 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 `read` grant for the domain.
- **Service calls** (turning a light on, setting a thermostat) require a `control` grant for the domain.
- A call that no grant authorizes is rejected, and the user is notified.

## Declaring capabilities

  1. 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. 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` you pass 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. Read and control through the SDK hooks. Reads need a matching `read` (or `control`) grant; service calls need `control`:
   
       ```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 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.

```jsonc
{ "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.

```jsonc
{ "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.

## See also

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

The SDK hooks every capability gates: useEntity, useService, and the rest.
  ### [Widget Security](/docs/widget-security)

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