---
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-08-10
---
# 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 call.

Access is **deny-by-default**: anything you don't declare is blocked at runtime.

> **Info:** This is the developer's view. For the homeowner-facing explanation of the sandbox, see <a href="/docs/dash/widget-security">Widget Security</a>.

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

## 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.** Every read and service call is checked against your grants at runtime; anything unmatched is blocked and the user is notified. Widget code cannot opt out.
- **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.