Agent skill
hass-declarative
Manage Home Assistant automations, scenes, and scripts declaratively via NixOS modules. Covers adding/editing/removing entities in the domain-based Nix structure, the ensureEnabled wrapper (initial_state enforcement), the sweep service that cleans orphaned entities, entity identity (IDs, slugs, unique_ids), the eval test assertions, and the build-time manifest. Trigger phrases: "add HA automation", "new scene", "new script", "remove automation", "declarative HA", "sweep unmanaged", "entity drift", "ghost entity", "orphaned automation", "HA domain file", "eval-automations test", "hass assertion", "ensureEnabled", "initial_state".
Install this agent skill to your Project
npx add-skill https://github.com/edmundmiller/dotfiles/tree/main/.pi/skills/hass-declarative
SKILL.md
HA Declarative Entity Management
All HA automations, scenes, and scripts are defined in Nix under
modules/services/hass/_domains/. A post-deploy sweep service removes
anything not in the declared set.
Architecture
_lib.nix ← shared helpers (ensureEnabled)
_domains/ ← domain files (Nix modules)
ambient.nix ← lighting schedules, plant lights
aranet.nix ← CO2 monitoring
conversation.nix ← voice intents
lighting.nix ← adaptive lighting, AL sleep mode
modes.nix ← DND, guest mode, everything_off script
sleep/default.nix ← bedtime flow, wake detection, 8Sleep sync
tv.nix ← TV idle timer, sleep timer
vacation.nix ← presence-based vacation mode
sweep-unmanaged.nix ← extracts declared IDs at build time
sweep-unmanaged.py ← runtime: removes orphans via WS API
_tests/eval-automations.nix ← structural assertions (flake check)
Entity Identity
How HA maps Nix config → entity registry:
| Domain | Nix declaration | Entity ID | Unique ID (registry) |
|---|---|---|---|
| automation | id = "winding_down"; |
automation.winding_down |
Same as id field |
| scene | name = "Good Morning"; |
scene.good_morning |
HA-generated UUID |
| script | script.everything_off = {...}; |
script.everything_off |
Same as attribute key |
Scene slugs: HA lowercases the name and replaces spaces/hyphens with underscores. Keep names ASCII-alphanumeric + spaces to avoid slug surprises.
Adding Entities
Automation
Add to the automation = lib.mkAfter (ensureEnabled [...]) list in the
appropriate domain file. Every automation must have a unique id field
— the sweep service uses it.
Always wrap with ensureEnabled (from _lib.nix) — it injects
initial_state = true so automations re-enable on HA restart. Without it,
HA silently persists "off" state from the entity registry and automations
stay disabled forever. The eval test catches missing wrappers at build time.
{ lib, ... }:
let
inherit (import ../_lib.nix) ensureEnabled;
in {
services.home-assistant.config.automation = lib.mkAfter (ensureEnabled [
{
alias = "Human-Readable Name";
id = "unique_snake_case_id";
description = "What it does";
trigger = { platform = "time"; at = "22:00:00"; };
condition = [];
action = [{ action = "scene.turn_on"; target.entity_id = "scene.foo"; }];
}
]);
}
Individual automations can override with initial_state = false if ever needed
(the // merge gives right-hand precedence), but we never want this in practice.
Scene
Add to scene = lib.mkAfter [...]. Scenes use name as their identity.
{
name = "My Scene";
icon = "mdi:icon-name";
entities = {
"light.some_light" = "on";
"switch.some_switch" = "off";
};
}
Script
Add to script = lib.mkAfter {...} (attrset, not list). The attribute
key becomes the entity_id.
my_script_key = {
alias = "Human Name";
icon = "mdi:icon";
sequence = [{ action = "light.turn_off"; target.entity_id = "light.foo"; }];
};
Or directly on config: script.my_key = {...}; (as in modes.nix).
Removing Entities
- Delete from the domain
.nixfile - Deploy (
hey nuc) hass-sweep-unmanagedservice auto-removes the orphan from HA's entity registry
No manual cleanup needed. Check sweep results:
hey nuc-service hass-sweep-unmanaged
ssh nuc "sudo journalctl -u hass-sweep-unmanaged --no-pager -n 30"
Sweep Service
sweep-unmanaged.nix creates a systemd oneshot that runs after HA starts.
Build time: Evaluates NixOS config to extract:
automation_ids— fromhaConfig.automation[].idscene_entity_ids— fromhaConfig.scene[].name→scene.<slug>script_entity_ids— fromhaConfig.scriptkeys →script.<key>
Writes these to a JSON manifest in the Nix store.
Runtime (sweep-unmanaged.py):
- Waits for HA to be ready (120s timeout)
- Connects via WebSocket, authenticates with
agent-automationJWT - Lists all entity registry entries
- For each
automation.*/scene.*/script.*not in the manifest:- Checks
platformto avoid removing integration-created entities - Removes from entity registry via
config/entity_registry/remove
- Checks
- Wipes UI YAML files (
automations.yaml,scenes.yaml,scripts.yaml)
Platform filtering (safety):
- Automations: only removes
platform = "automation"(YAML-sourced) - Scenes: only removes
platform = "homeassistant"(YAML-sourced) - Scripts: only removes
platform = "script"(YAML-sourced) - Integration-created entities (Ecobee scenes, etc.) are never touched
Eval Test Assertions
_tests/eval-automations.nix runs as nix flake check and pre-commit hook.
Tests structural properties:
- Every automation has
initial_state = true— catches missingensureEnabledwrappers - Required automations/scenes exist
- Time guards present on wake detection (the "4:47 AM fix")
- Good Morning has presence-aware conditions
- Winding Down resets awake booleans
Add assertions when adding automations with critical invariants.
initial_state enforcement
The initial_state assertion is global — it iterates all automations in the
final merged config. No per-automation opt-in needed. If you add an automation
anywhere without ensureEnabled, the build fails:
automation 'My Thing' missing initial_state = true (use ensureEnabled from _lib.nix)
Why this matters: HA's initial_state defaults to "restore from entity
registry." If an automation was ever toggled off in the UI (or the registry
entry drifts), it stays off across restarts — silently. With
configWritable = false, the UI toggle is especially dangerous since there's
no way to re-enable it without redeploying. ensureEnabled + the eval
assertion make this class of bug impossible.
Debugging
# Check what the sweep would do (dry-run)
ssh nuc "sudo systemctl cat hass-sweep-unmanaged" # see ExecStart paths
ssh nuc "sudo /path/to/python3 /path/to/sweep-unmanaged.py /path/to/manifest.json --dry-run"
# View the build-time manifest
nix eval --json '.#nixosConfigurations.nuc.config.services.home-assistant.config.automation' 2>/dev/null | python3 -c "import json,sys; print([a['id'] for a in json.load(sys.stdin) if a.get('id')])"
# Run eval assertions locally
nix build '.#checks.x86_64-linux.ha-automation-assertions' --dry-run
References
| File | Contents |
|---|---|
references/entity-lifecycle.md |
How HA stores entities internally, the .storage files, and what "ghost" entities are |
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
zbench
Benchmark interactive zsh performance with zsh-bench and track regressions. Use when benchmarking shell startup, comparing zsh latency after config changes, investigating slow shell, or running git bisect on performance. Trigger phrases: "benchmark zsh", "shell is slow", "zbench", "zsh-bench", "shell startup time", "profile zsh", "zsh performance".
nix-rebuild
Rebuild nix-darwin/NixOS system after dotfiles changes. Use when config files managed by Nix (lazygit, ghostty, etc.) need to be regenerated, or after editing any .nix file in the dotfiles repo.
hass-config-flow
Interact with Home Assistant via the REST API on a NixOS host. Use when adding integrations, querying entities, managing config flows, creating API tokens, or automating HA setup programmatically. Also covers identifying device protocols (Matter, Zigbee, Thread, HomeKit) from the device registry. Trigger phrases: "add HA integration", "configure home assistant", "query HA entities", "create HA token", "HA REST API", "pair homekit", "set up matter in HA", "add spotify to HA", "is this device zigbee or thread", "what protocol is this device", "move devices to ZHA", "identify matter devices".
agenix-secrets
Create, edit, and wire up agenix-encrypted secrets in this dotfiles repo. Use when adding API keys, tokens, credentials, passwords, or any sensitive values to NixOS host configs. Trigger phrases: "add a secret", "encrypt with agenix", "new age secret", "hide this value", "agenix secret".
linear
Read-only Linear issue access via the Linear GraphQL API.
jut
Jujutsu version control through jut, a human and agentic framework around jj. Use for: check status, view changes, commit work, create branches, push, pull, create PRs, squash commits, reword messages, absorb changes, undo operations, view history. Complements jj — use jut for opinionated workflows, drop into raw jj for everything else.
Didn't find tool you were looking for?