git:20260901.ff0c382 to git:20260905.0c707c7

34 added, 157 removed. Audit B to B.

# claude-code-homeassistant-hermit
A Home Assistant domain layer for `claude-code-hermit`: skills, subagents, a safety hook, and a TypeScript CLI (run by bun) for bulk work.
- ## Plugin Structure
-
- - `skills/ha-*/` — workflow skills namespaced as `/claude-code-homeassistant-hermit:ha-*`
- - `skills/domain-brainstorm/` — on-demand capability-gap brainstorm: reads entity inventory, automation/script listing, and operator intent to surface at most 2 `[prefix]`-tagged improvement proposals. Operator-invoked only. After ≥8 runs, a shared capability-brainstorm segment below the kill thresholds is a class-level signal to raise with the operator, not a per-skill retirement trigger.
- - `agents/` — `ha-safety-reviewer`, `ha-automation-builder`, `ha-pattern-analyst`
- - `hooks/` — `mcp-safety-gate.ts` + `hooks.json` (PreToolUse on `mcp__homeassistant__.*` — the whole server namespace; read-only tools are allow-listed inside the gate)
- - `bin/ha-agent-lab` + `src/*.ts` — TypeScript CLI run by bun (REST client, policy engine, simulation, apply)
- - `settings.json` — pre-approved permissions for safe CLI and read-only MCP tools
- - `state-templates/CLAUDE-APPEND.md` — block injected into the target project's `CLAUDE.md` by `hatch`
- - `.claude-plugin/plugin.json` — plugin manifest
- - `.claude-plugin/hermit-meta.json` — hermit-internal fields (`required_core_version`, `requires`, `hermit.boot_skill`)
-
- ## Hatch target routing
+ ## Structure
- `/hatch` Step 1 runs `.claude-code-hermit/bin/hermit-run domain-hatch preflight claude-code-homeassistant-hermit`; core's `scripts/domain-hatch.ts` owns install-scope detection, target resolution, and stamping `hatch-options.json`. The preflight verdict hands back `target`, `target_file`, `target_default`, and `needs_target_question` — Step 6 only surfaces the Visibility prompt when asked to, records the answer with `domain-hatch ensure-target claude-code-homeassistant-hermit --target <choice>`, then writes the block with `domain-hatch sync-block claude-code-homeassistant-hermit`. Hatch appends when the marker is absent and skips otherwise; refreshing the block on a version bump is `hermit-evolve`'s job.
+ - `skills/ha-*/`: workflow skills (`/claude-code-homeassistant-hermit:ha-*`); `ha-boot` is the single entry point (starts the hermit session, checks HA connectivity). `skills/domain-brainstorm/` is operator-invoked only.
+ - `agents/`: `ha-safety-reviewer`, `ha-automation-builder`, `ha-pattern-analyst`
+ - `hooks/mcp-safety-gate.ts` + `hooks.json`: PreToolUse on `mcp__homeassistant__.*`, the whole server namespace; read-only tools are allow-listed inside the gate
+ - `bin/ha-agent-lab` + `src/*.ts`: the CLI (REST client, WebSocket client, policy engine, simulation, apply). `src/policy.ts` is shared by the CLI and the hook.
+ - `settings.json`: pre-approved permissions for safe CLI and read-only MCP tools
+ - `state-templates/CLAUDE-APPEND.md`: block injected into the target project by `hatch`
+ - `.claude-plugin/hermit-meta.json`: `required_core_version`, `requires`, `hermit.boot_skill`
+ - `SAFETY.md`: the safety model; `docs/cli-reference.md`: command usage examples
- ## Core Rules
+ ## Rules
- - `/claude-code-homeassistant-hermit:ha-boot` is the single entry point — starts the hermit session and checks HA connectivity.
- - Never commit real HA URLs, tokens, or device inventories.
- - Actuation of sensitive domains (`lock`, `alarm_control_panel`, security-related `cover`/`button`/`switch`) is gated by `ha_safety_mode` in `.claude-code-hermit/config.json` (absent = `strict`). Under `strict` (default): never autonomously actuate — blocked work becomes a proposal. Under `ask`: the operator is prompted before any sensitive actuation (both YAML apply and direct MCP calls). When in doubt about a new domain, default to sensitive. See `SAFETY.md` for the full safety model.
- - Uncertain entities default to sensitive. Blocked work becomes a proposal.
- - Use the stored language from OPERATOR.md (`## HA hermit` section) for all user-facing output.
+ - Never commit real HA URLs, tokens, or device inventories. Check credential state with `bin/ha-agent-lab boot status`, never `cat .env` or `echo $HOMEASSISTANT_TOKEN` (core's seeded rules deny `cat .env*`, and expanding a credential var puts the value in the transcript).
+ - Actuation of sensitive domains (`lock`, `alarm_control_panel`, security-related `cover`/`button`/`switch`) is gated by `ha_safety_mode` in `.claude-code-hermit/config.json` (absent = `strict`). Under `strict`, never actuate autonomously; blocked work becomes a proposal. Under `ask`, the operator is prompted before any sensitive actuation, YAML apply or direct MCP call alike. Uncertain entities and new domains default to sensitive. Full model: `SAFETY.md`.
+ - Use the language stored in OPERATOR.md's `## HA hermit` section for all user-facing output. That section is operator-curated config (locale today); auto-memory holds Claude-derived house knowledge.
- Prefer the CLI over ad-hoc reasoning when a helper exists.
- - Don't overengineer.
- ## Memory Conventions
-
- - **Auto memory** (`~/.claude/projects/<key>/memory/`): Claude-derived knowledge — learned patterns, house profile observations, known issues, cross-session suppression signals. Platform-managed; loaded automatically at each session start.
- - **`.claude-code-hermit/OPERATOR.md`** — operator-set config (locale today; future room defaults, alert preferences, etc.). Curated by the operator under a `## HA hermit` section. Read by the CLI and by skills/agents at session start.
- - `.claude-code-hermit/raw/` — HA context snapshots, normalized data, audits, staged automation YAML (ephemeral; aged out by retention).
- - `.claude-code-hermit/compiled/` — durable domain outputs (morning briefs, house profile) injected at session start.
- - `.claude-code-hermit/state/` — machine state (runtime, reflection, micro-proposals, alert state).
- - `.claude-code-hermit/proposals/` — PROP-NNN improvement proposals.
- - `.claude-code-hermit/sessions/S-*-REPORT.md` — archived session reports.
-
## MCP vs CLI
- - **Home Assistant MCP Server** (`homeassistant`): read-only live ops by default — `GetLiveContext`, `GetDateTime`. `Hass*` intent tools (`HassTurnOn`, `HassLightSet`, `HassSetPosition`, `HassFanSetSpeed`, etc.) are hard-blocked unless `ha_assist_control_enabled: true` is set in `config.json` (set during hatch Step 6.55). When enabled, HA's own expose-to-Assist gate is the control boundary — the gate defers to it rather than blocking.
- - **CLI** (`bin/ha-agent-lab`): build and analysis operations — context refresh, YAML simulation, policy checks, apply, audits, structural writes (helpers/areas/registries), and `ha trigger-automation`.
-
- MCP tool IDs follow the pattern `mcp__homeassistant__*`. The `homeassistant` name is required — the safety hook matches on it.
-
- ## CLI Commands
-
- ```
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha refresh-context [--incremental]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha simulate <artifact>
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha validate-apply <artifact> [--reload automation|script|scene]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha policy-check <entity_id_or_yaml>
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha audit-automations
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha audit-scripts
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha list-automations
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha list-scripts
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha list-scenes
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha delete-automation <id>
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha delete-script <id>
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha delete-scene <id>
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha get-automation-config <id>
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha get-script-config <id>
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha get-scene-config <id>
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha automation-diff
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha snapshot-states [--name <label>] [--domains light,cover,climate,switch] [--entities <id> …]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha restore-states <artifact> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha integration-health
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha updates [--digest]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha fetch-history [--window-days N] [--entities <glob> …] [--include-transitions]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha probe <path>
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha render-template <file|->
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha check-config
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha call-service <domain.service> [--data <json>] [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha set-core-config [--latitude N] [--longitude N] [--elevation N] [--unit-system metric|us_customary] [--currency CODE] [--time-zone TZ] [--country CODE] [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha error-log
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha logbook [--window-days N] [--entity <entity_id>]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha system-log
- # WebSocket-backed structural commands (helpers, areas, registries). Writes are gated by ha_safety_mode.
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha list-helpers [--type <helper_type>]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha create-helper <type> <json> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha delete-helper <type> <id> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha list-areas
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha create-area <name> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha delete-area <id> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha rename-area <area_id> --name <name> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha set-area-icon <area_id> --icon <icon> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha set-area-floor <area_id> --floor <floor_id> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha set-area-labels <area_id> --labels <label> [<label> ...] [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha list-floors
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha create-floor <name> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha delete-floor <id> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha list-labels
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha create-label <name> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha delete-label <id> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha list-entities --registry
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha rename-entity <entity_id> --name <name> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha set-entity-area <entity_id> --area <area_id> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha set-entity-enabled <entity_id> --enabled true|false [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha set-entity-icon <entity_id> --icon <icon> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha set-entity-hidden <entity_id> --hidden true|false [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha set-entity-labels <entity_id> --labels <label> [<label> ...] [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha set-entity-categories <entity_id> --categories <json> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha set-entity-aliases <entity_id> --aliases <alias> [<alias> ...] [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha list-devices
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha set-device-area <device_id> --area <area_id> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha rename-device <device_id> --name <name> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha list-dashboards
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha get-dashboard [--url-path <url_path>]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha apply-dashboard <artifact> [--url-path <url_path>] [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha create-dashboard <json> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha delete-dashboard <dashboard_id> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha list-exposed-entities
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha expose-entity --entity-ids <id> [<id> ...] --assistants <assistant> [<assistant> ...] --expose true|false [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha list-backups
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha create-backup --agent-ids <id> [<id> ...] [--name <name>] [--password <password>] [--include-addons <slug> [<slug> ...]] [--include-all-addons] [--include-database true|false] [--include-folders <folder> [<folder> ...]] [--include-homeassistant true|false] [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha list-blueprints <domain>
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha import-blueprint <domain> <url> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha get-energy-prefs
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha set-energy-prefs <json> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha reload-entry <entry_id> [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha disable-entry <entry_id> --disabled true|false [--confirm]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha trigger-automation <automation_id>
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab boot status [--probe]
- ${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab boot store --language <locale> --url <url> [--token <token>]
- bun test
- ```
-
- Run `--help` for current flags. Source of truth: `src/cli.ts`.
+ - **MCP server `homeassistant`**: read-only live ops by default (`GetLiveContext`, `GetDateTime`). `Hass*` intent tools (`HassTurnOn`, `HassLightSet`, ...) are hard-blocked unless `ha_assist_control_enabled: true` in `config.json` (set during hatch); when enabled, HA's own expose-to-Assist gate is the control boundary and the hook defers to it. The server name `homeassistant` is required: the hook matches on it.
+ - **CLI `bin/ha-agent-lab`**: build and analysis operations: context refresh, YAML simulation, policy checks, apply, audits, structural writes (helpers, areas, registries, dashboards), `ha trigger-automation`. Invoke as `${CLAUDE_PLUGIN_ROOT}/bin/ha-agent-lab ha <command>`; `--help` and `src/cli.ts` are the command surface, `docs/cli-reference.md` has examples. Writes take `--confirm` and are gated by `ha_safety_mode`.
## HA API gotchas
- - REST API docs: https://developers.home-assistant.io/docs/api/rest/
- - WebSocket API docs: https://developers.home-assistant.io/docs/api/websocket/
-
- Before changing HA endpoint usage, verify against upstream (WebFetch or the `find-docs` skill) or probe a live instance with `./bin/ha-agent-lab ha probe <path>`. Do not assume an endpoint exists.
+ REST docs: https://developers.home-assistant.io/docs/api/rest/ ; WebSocket docs: https://developers.home-assistant.io/docs/api/websocket/ . Before changing endpoint usage, verify against upstream or probe a live instance with `./bin/ha-agent-lab ha probe <path>`; do not assume an endpoint exists.
- - Automations have no bulk REST listing. Enumerate via `/api/states` (filter `domain=automation`), fetch each config via `/api/config/automation/config/{automation_id}`. YAML-packaged automations with a slug `id` (e.g. `accao_boa_noite_via_notificacao`) carry an `id` in their state attributes and are REST-retrievable via that slug — the `/api/states`+id approach covers 100% in practice. `config/automation/list` WebSocket command returns "Unknown command" on real HA instances — do not rely on it.
- - `POST /api/config/{automation|script}/config/{id}` — create/update (upsert). URL `id` is sufficient; body `id` field is ignored by HA. Returns `{"result":"ok"}` on success. Returns 403 if HA is in YAML config mode (REST config API unavailable).
- - `DELETE /api/config/{automation|script}/config/{id}` — remove config. **A missing id returns 400** (not 404) with `{"message":"Resource not found"}` — do not special-case 404. All HA error responses carry `{"message":"..."}` — surface it verbatim.
- - After `POST`, `GET` reflects the change synchronously (verified against HA 2026.x). No retry or delay needed for verify calls.
- - `--reload {automation|script|scene}` in `ha validate-apply` is overloaded: it controls both the REST push endpoint and the reload service call. There is no push-only mode; add `--no-reload` if that use case arises. Scenes use the same REST config API (`/api/config/scene/config/{id}`) and `scene.reload` service as automation/script — no special path.
- - `POST /api/template` returns the rendered template as a **raw plain-text body**, not JSON (confirmed against HA core source and live) — `client.post()`'s unconditional `JSON.parse` would throw `Malformed JSON` on any non-JSON-looking render (e.g. `idle`). `GET /api/error_log` is the same: HA serves the raw log file, not JSON. Use `client.postText()`/`client.getText()` (raw-response variants) for endpoints like these.
- - **`ha call-service` is gated per-entity/service (`gateServiceCall` in `policy.ts`), not by the structural gate.** Concrete sensitive entity targets block as a proposal under `strict` and need `--confirm` under `ask`; unresolvable entity/device/area/floor/label selectors and malformed target shapes hard-block in both modes. Non-sensitive calls proceed in both modes since call-service exists for maintenance (reloads, `recorder.purge`, `notify.*`). Reuses the same fail-closed entity-extraction logic as the MCP safety hook (`extractEntityIds`/`hasUnresolvableTarget`/`isWellFormedEntityId`, relocated to `policy.ts`) plus a `call-service`-only `hasMalformedTargetShape` guard for wrong-shaped `--data` (target as an array, non-string entity_id) that the shared extractors can't see. When touching this gate, keep `tests/gate-corpus.test.ts`/`tests/gate-fuzz.test.ts` green — they pin the MCP hook's byte-identical output.
- - **The `update` domain (`update.install` etc.) has its own carve-out inside `gateServiceCall`, independent of `ha_safety_mode` and of `SENSITIVE_DOMAINS`.** With `ha_update_auto_apply` unset/false in `.claude-code-hermit/config.json`, any `update.*` call with no other sensitive entity riding along is blocked outright (surface as a proposal) — closing what would otherwise be an ungated actuation path, since `update` was never in `SENSITIVE_DOMAINS`. With the flag `true`, the same call requires `--confirm` on every invocation, in both `strict` and `ask` — the flag authorizes the call class, `--confirm` authorizes each instance; neither alone is enough. A call that also references a genuinely sensitive entity (lock/alarm) is unaffected and still hard-blocks under strict via the existing path. See `skills/ha-apply-update/SKILL.md` for the tier rule this feeds into (add-ons/HACS may auto-apply; Core/OS/Supervisor always wait for an explicit operator confirm regardless of the flag).
- - **`GET /api/error_log` 404s unless HA registered `DATA_LOGGING`** (HA core only wires up the view `if DATA_LOGGING in hass.data`) — a deployment characteristic, not a bug; the command surfaces the 404 verbatim. Confirmed 404 on paulinho (2026.6.4); `ha logbook`/`ha system-log` don't have this dependency and work on the same instance.
- - `GET /api/logbook/<timestamp>` only supports filtering by **one** entity via `?entity=<id>` (not comma-separated or glob, unlike `filter_entity_id` on `/api/history/period/`) — `ha logbook --entity` is singular for this reason.
+ - Automations have no bulk REST listing: enumerate via `/api/states` (filter `domain=automation`), then fetch each config from `/api/config/automation/config/{automation_id}`. YAML-packaged automations with a slug `id` carry it in state attributes and are retrievable the same way. The `config/automation/list` WebSocket command returns "Unknown command" on real instances.
+ - `POST /api/config/{automation|script}/config/{id}` upserts; the URL `id` is what counts, the body `id` is ignored. Returns `{"result":"ok"}`, or 403 when HA is in YAML config mode. `GET` reflects the change synchronously; no retry needed.
+ - `DELETE .../config/{id}` on a missing id returns 400 (not 404) with `{"message":"Resource not found"}`. All HA error responses carry `{"message":"..."}`; surface it verbatim.
+ - `--reload {automation|script|scene}` in `ha validate-apply` controls both the REST push endpoint and the reload service call; there is no push-only mode. Scenes use the same REST config API and `scene.reload`.
+ - `POST /api/template` and `GET /api/error_log` return raw text, not JSON; use `client.postText()`/`client.getText()` in `src/ha-api.ts`. `GET /api/error_log` 404s on deployments where HA never registered `DATA_LOGGING` (a deployment characteristic; the command surfaces the 404 verbatim); `ha logbook`/`ha system-log` have no such dependency.
+ - `GET /api/logbook/<timestamp>` filters by one entity only (`?entity=<id>`), unlike `filter_entity_id` on `/api/history/period/`; hence `ha logbook --entity` is singular.
+ - `ha call-service` is gated per entity/service by `gateServiceCall` in `policy.ts`, not by the structural gate: concrete sensitive targets block as a proposal under `strict` and need `--confirm` under `ask`; unresolvable selectors and malformed target shapes hard-block in both modes; non-sensitive maintenance calls (reloads, `recorder.purge`, `notify.*`) proceed. It reuses the hook's fail-closed entity extraction (`extractEntityIds`/`hasUnresolvableTarget`/`isWellFormedEntityId`) plus a `hasMalformedTargetShape` guard for wrong-shaped `--data`.
+ - The `update` domain has its own carve-out in `gateServiceCall`, independent of `ha_safety_mode` and `SENSITIVE_DOMAINS`: with `ha_update_auto_apply` unset, any `update.*` call is blocked (surface as a proposal); with it `true`, every call still needs `--confirm`. The flag authorizes the class, `--confirm` each instance. A call that also touches a lock/alarm entity still hard-blocks under strict. The Core/OS/Supervisor tier rule lives in `skills/ha-apply-update/SKILL.md`.
### WebSocket commands (`src/ha-ws.ts` + `src/structure.ts`)
- - Helpers, areas, and entity/device registries have **no REST endpoint** — they are reachable only over `wss://<host>/api/websocket`. `HomeAssistantWsClient` opens a single-shot connection per CLI invocation (auth handshake → commands → close), reusing the same URL selection and token as the REST client.
- - Command types: helpers `<type>/create|list|delete` (8 types: `input_boolean|input_number|input_text|input_select|input_datetime|timer|counter|schedule`); areas `config/area_registry/create|list|delete`; registries `config/entity_registry/list|update` and `config/device_registry/list|update`; dashboards `lovelace/dashboards/list|create|delete`, `lovelace/config`, `lovelace/config/save` (shapes cross-checked against `home-assistant/core`'s `DictStorageCollectionWebsocket` generic collection handler — the same pattern as areas/helpers).
- - **Confirm the exact command `type` and payload fields against a live instance before relying on them** (the docs index documents only the auth/result envelope). Probe pattern: run the new commands against a real HA and read the responses.
- - **All WS mutations are gated by `ha_safety_mode`** (`gateStructuralMutation` in `policy.ts`). Reads are never gated. Under `strict` (default) a mutation is refused (`blocked: true`) — surface it as a proposal. Under `ask` it requires `--confirm`, which the main session passes after prompting the operator (the CLI is non-interactive). Every mutation writes an audit report to `.claude-code-hermit/raw/` (`audit-ha-ws-*`).
-
- ## Development constraints
-
- - When aligning with a new hermit version, include `docs/` in terminology sweeps — `docs/knowledge-schema.md` and other doc files carry hermit-facing terms that go stale. Verification: `grep -rn "<old-term>" skills/ agents/ state-templates/ docs/ CLAUDE.md .claude-plugin/`
- - The CLI and both hooks are TypeScript run directly by bun (`bun src/cli.ts`, `bun hooks/*.ts`) with zero runtime dependencies — bun is guaranteed by the core hermit requirement. No shipped code runs Python; the only Python in the test suite is a fixture (`tests/gate-corpus.test.ts` replays the retired Python hooks from git history at `42c0c8f~1`, `tests/yaml-parity.test.ts` compares against PyYAML).
- - The safety hook fails closed — if an MCP call's target cannot be resolved to concrete entity IDs, it is blocked. Changes to `hooks/mcp-safety-gate.ts` or `src/policy.ts` must keep `tests/gate-corpus.test.ts` (golden byte-equivalence vs the retired Python gate) and `tests/gate-fuzz.test.ts` (fail-closed property) green.
- - Read credentials via the CLI (`bin/ha-agent-lab boot status`), never `cat .env` / `echo $HOMEASSISTANT_TOKEN` — core's seeded permission rules deny `cat .env*` dumps, and expanding an unlisted credential var still puts the value in the transcript.
- - Agent references in skill instructions must use the full namespaced form (e.g., `claude-code-homeassistant-hermit:ha-safety-reviewer`). Bare names will fail at dispatch.
+ - Helpers, areas, and entity/device registries have no REST endpoint; they are reachable only over `wss://<host>/api/websocket`. `HomeAssistantWsClient` opens one connection per CLI invocation (auth handshake, commands, close), reusing the REST client's URL selection and token.
+ - Command types: helpers `<type>/create|list|delete` (`input_boolean|input_number|input_text|input_select|input_datetime|timer|counter|schedule`); areas `config/area_registry/*`; registries `config/entity_registry/list|update`, `config/device_registry/list|update`; dashboards `lovelace/dashboards/list|create|delete`, `lovelace/config`, `lovelace/config/save`. The docs index documents only the auth/result envelope, so confirm a new command's exact `type` and payload against a live instance.
+ - All WS mutations are gated by `ha_safety_mode` via `gateStructuralMutation` in `policy.ts`; reads never are. Under `strict` a mutation is refused (`blocked: true`) and surfaced as a proposal; under `ask` it needs `--confirm`, which the main session passes after prompting the operator (the CLI is non-interactive). Every mutation writes an `audit-ha-ws-*` report to `.claude-code-hermit/raw/`.
- ## Routines and Scheduled Checks
+ ## Routines
- `hatch` registers entries in `.claude-code-hermit/config.json`:
+ `hatch` registers routines (`daily-ha-context`, `morning-brief`, `evening-brief`; unified vs legacy brief mode is decided at hatch time) and proposal-producing scheduled checks (`ha-patterns`, `ha-safety-audit`, `ha-integration-health`, `ha-update-check`, run by core's `scheduled-checks` routine via `reflect --scheduled-checks`) in `.claude-code-hermit/config.json`. Schedules live there, not here. Core's `hermit-routines load` activates them.
- - **Routines**: `daily-ha-context` (08:30 daily, enabled), `morning-brief` (unified mode: 08:30, enabled, replaces core `morning`; legacy mode: 09:00, disabled — determined at hatch time), `evening-brief` (22:30 daily, enabled, run_during_waiting; subsumes core `evening`).
- - **Scheduled checks** (driven by the core `scheduled-checks` routine via `reflect --scheduled-checks`, proposal-producing): `ha-patterns` (weekly), `ha-safety-audit` (weekly), `ha-integration-health` (daily), `ha-update-check` (daily).
+ ## Hatch target routing
- In interactive sessions, run `/claude-code-hermit:hermit-routines load` once to activate scheduled routines. In always-on deployments they load automatically.
+ Core's `scripts/domain-hatch.ts` owns target resolution and `hatch-options.json`. `/hatch` Step 1 runs `.claude-code-hermit/bin/hermit-run domain-hatch preflight claude-code-homeassistant-hermit`; Step 6 asks the Visibility question only when `needs_target_question` says so, records it with `domain-hatch ensure-target claude-code-homeassistant-hermit --target <choice>`, and writes the block with `domain-hatch sync-block claude-code-homeassistant-hermit`. Hatch appends when the marker is absent and skips otherwise; refreshing the block on a version bump is `hermit-evolve`'s job.
## Development
- Test locally against a target project without installing:
-
- ```
- cd /path/to/target-project
- claude --plugin-dir /path/to/claude-code-homeassistant-hermit
- ```
+ `claude --plugin-dir /path/to/claude-code-homeassistant-hermit` from a target project, then `/claude-code-homeassistant-hermit:hatch`. Tests: `bun test` from this directory.
- Then run `/claude-code-homeassistant-hermit:hatch` in the target.
+ - The CLI and both hooks are TypeScript run directly by bun with zero runtime dependencies. Python is test-only: `tests/gate-corpus.test.ts` replays the retired Python hooks from git history and `tests/yaml-parity.test.ts` compares against PyYAML. The suite needs full git history and Python with `python-dotenv` and `PyYAML`; set `GATE_PARITY_PYTHON` when that interpreter is outside PATH.
+ - The safety hook fails closed: an MCP call whose target cannot be resolved to concrete entity IDs is blocked. Changes to `hooks/mcp-safety-gate.ts` or `src/policy.ts` must keep `tests/gate-corpus.test.ts` (golden byte-equivalence with the retired gate) and `tests/gate-fuzz.test.ts` (fail-closed property) green.