hassette
CLAUDE.md81 starsChanged 7 months agoMITView on GitHub
Hassette is an async-first Python framework for building Home Assistant automations. It emphasizes type safety (Pydantic models), dependency injection (FastAPI-style), and async/await patterns. Python 3.11+. The repo is a uv workspace of three packages released in lockstep from one release-please train (single "." component, one release PR, one tag): hassette (root), wire/ → hassette-wire, and client/ → hassette-client. Each pins the ones it depends on exactly (hassette-wire==X). The root version is bumped by release-please's python release-type; the members' version lines…
- Installs packages
We split this CLAUDE.md into the rules it gives an agent
9 rules. About 0 are common, 2 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Very specific: line 49
After publish-docker, the bump-addon job commits the add-on's version and a CHANGELOG entry to NodeJSmith/hassette-addon (spec 129 D12), …
- Distinctive: line 91
A pointer names something that exists: When you move or rename a doc section, grep for references to it (including code comments) and …
- Very specific: line 112
Test infrastructure, factories, and mock strategy: .claude/rules/test-conventions.md and tests/TESTING.md.
- Very specific: line 113
Run pytest with -n 4, never -n auto.
- Distinctive: line 114
Run any test you fix or modify before committing — code inspection misses marker filtering, warning config, fixture scoping, and async …
- Very specific: line 120
UI changes: use the demo stack (mise run demo) for visual QA, and capture screenshots only via scripts/capture_screenshots.py, never raw …
- Very specific: line 124
Issues: read .claude/reference/github-workflow.md before filing — titles, required type/area/size labels, topic labels, body sections.
- Very specific: line 138
Read .claude/rules/changelog-quality.md before creating a PR with a breaking change or reviewing a release-please PR.
- Very specific: line 149
No blanket # type: ignore — use # pyright: ignore[reportXxx].
≈ “Common” is an estimate: at least 1 in 5 of the 25,680 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md
Hassette is an async-first Python framework for building Home Assistant automations. It emphasizes type safety (Pydantic models), dependency injection (FastAPI-style), and async/await patterns. Python 3.11+.
Common Commands
uv sync # install dependencies
uv run nox -s dev # tests, local dev loop
uv run nox -s tests # tests across Python 3.11–3.14 (CI)
uv run nox -s tests_with_coverage # backend coverage — never `pytest --cov` (under-reports; see tests/TESTING.md)
uv run pytest tests/integration/test_api.py::test_function_name -v
cd frontend && npm install # once per worktree — worktrees don't share node_modules
cd frontend && npm run test:coverage # frontend coverage
prek -a # lint + format (all pre-commit hooks)
prek run pyright -a --stage pre-push # type check — pyright is a pre-push hook, so `prek -a` skips it
uv run mkdocs serve # docs site
hassette run # start the server
hassette status | app | listener | log | job # query a running instance (--app, --instance, --since, --json)
# Deterministic telemetry DB for QA/screenshots/demos (scenarios: healthy, empty, degraded,
# error, large-volume, lifecycle, adversarial; generators in scripts/seed_scenarios/)
uv run python scripts/seed_db.py --scenario healthy --output /tmp/hassette-healthy.dbWorkspace Packages
The repo is a uv workspace of three packages released in lockstep from one release-please train (single "." component, one release PR, one tag): hassette (root), wire/ → hassette-wire, and client/ → hassette-client. Each pins the ones it depends on exactly (hassette-wire==X). The root version is bumped by release-please's python release-type; the members' version lines and every exact pin carry an # x-release-please-version marker that a generic extra-file rewrites — don't remove the marker when editing those lines. release-please-config.json lists every file carrying a marker under extra-files, and the release build fails if a member's version differs from hassette's.
The release workflow builds each package into its own dist/<name>/ directory and publishes them in three separate jobs, chained with needs: in order: publish-pypi-wire, then publish-pypi-client, then publish-pypi-hassette. The order is load-bearing (each pins the previous), so don't collapse them back into one job. Each package gets its own GitHub Environment — release-wire, release-client, release — not a shared one: PyPI's pending trusted-publisher match (for a project that doesn't exist on PyPI yet) is keyed on (repository, workflow file, environment name) alone, since there's no project to disambiguate by, so two packages sharing one environment register as the same pending publisher and PyPI rejects the second. release-wire/release-client already exist (created via gh api, deployment branch policy main + v** tags copied from release; no secrets/variables — trusted publishing is OIDC-only). Each new package still needs its own trusted publisher registered by hand on PyPI before its first publish (workflow release-please.yml, its own environment name) — that side isn't automatable from here. Run a member's tests with uv run nox -s wire / uv run nox -s client; uv run nox -s client_compat checks that the newest published client release reachable from HEAD handles every web API response HEAD sends (parsing, probe bodies, error mapping; fixtures generated per seed scenario). A new workspace member touches every one of these:
wire/**-style path filters in thetests,lint,e2e-testsanddocker-testsworkflows, plus auv run nox -s <member>line intests.yml'sworkspace-membersjobpyrightconfig.json,prek.toml, and[tool.house-lint]/norecursedirsinpyproject.tomlruff.toml's per-file-ignores, if the member's tests useassert(a<member>/tests/**/*.py→["S101"]entry)release-please-config.json'sextra-files(the# x-release-please-versionmarker on the member'spyproject.tomldoes nothing without this)Dockerfile, if the member is a runtime dependency ofhassetterather than a dev-only toolrenovate.json, if the member is pinned exactly by another workspace member (stops Renovate from bumping a pin release-please owns)release-please.yml: a newpublish-pypi-<name>job reusing thepublish-pypi-stepsYAML anchor with its ownPACKAGE_NAME/PACKAGES_DIRenv (the comment above those jobs says why it can't be a reusable workflow or composite action), chained after the member it depends on vianeeds:, with its own GitHub Environment (created the same way asrelease-wire/release-client, same branch policy) — and add the job torelease-verify'sneeds:and result checks
After publish-docker, the bump-addon job commits the add-on's version and a CHANGELOG entry to NodeJSmith/hassette-addon (spec 129 D12), using the release-please app's token scoped to that repo; tools/release/bump_addon.py holds its stable-only, no-downgrade and equal-is-a-no-op rules, and release-verify fails the release if it fails.
Architecture
| Component | Location | Role |
Hassette | src/hassette/core/core.py | Coordinator: connects to HA over WebSocket, manages app lifecycle and services |
App | src/hassette/app/app.py | Base class for user automations, generic over AppConfig. Hooks: on_initialize, on_shutdown. Each app gets its own Bus, Scheduler, Api, StateManager |
| Bus | src/hassette/bus/ | Event pub/sub: on_state_change, on_attribute_change, on_call_service, on, and one-shot wait_for. Glob patterns, predicates, conditions, debounce, throttle |
| Scheduler | src/hassette/scheduler/ | schedule(func, trigger) plus run_in/run_once/run_every/run_daily/run_cron. Triggers in hassette.scheduler.triggers; custom ones implement TriggerProtocol. Job groups and jitter |
| Api | src/hassette/api/ | HA REST/WebSocket: get_state, get_states, call_service, set_state, fire_event. Api.helpers (HelperClient) does CRUD on HA helper entities with per-domain @overloads |
| StateManager | src/hassette/state_manager/ | Cached, typed state: self.states.light, self.states[CustomState], self.states.get("light.kitchen") |
| Event handling | src/hassette/event_handling/ | predicates (P), conditions (C), accessors (A), dependencies (D) |
| Resources | src/hassette/resources/ | Resource/Service base classes: lifecycle hooks, child tracking, supervision |
| Web | src/hassette/web/ | FastAPI backend for the monitoring UI; SPA in frontend/ |
Type conversion registries: STATE_REGISTRY (HA entity types → model classes), TYPE_REGISTRY (scalar field conversion).
Rules that bite everywhere:
- Bus registration methods are
asyncand must be awaited.name=is required on every DB-registered listener (ListenerNameRequiredErrorotherwise);wait_foris the exception — its name is auto-generated. - Handlers get only the parameters their signature declares. Don't pad a handler with an unused
event: Event[Any]just to match siblings. - One live
Hassette()per process at a time. Process-global state assumes it; don't add concurrency guards for it.
Subsystem internals live in path-scoped rules under .claude/rules/ and load automatically when you work in the matching files: core-startup.md (startup readiness, app bootstrap gate, StateProxy), resource-lifecycle.md (lifecycle, teardown reports), bus-internals.md, web-api.md, frontend-css.md, demo-and-screenshots.md, and tests-*.md rules for several test directories with notable local fixtures (not every directory has one — check its conftest.py). New subsystem guidance goes there too, not in nested CLAUDE.md files.
Designing the Framework
Hassette is a framework. Its real callers are user apps in other repositories, which this repo never sees.
- In-repo usage is not evidence of demand. Grepping this repo measures blast radius (call sites to update), not whether users rely on something or struggle with it.
examples/,docs/, and internal apps only show what they happen to demonstrate. Don't dismiss an edge case or rank one API path over another because nothing here exercises it — assume users will. API changes still need changelog entries and migration guidance with zero in-repo callers. - Keep the public surface small.
Appcarries almost nothing; a new helper goes on the most specific component whose role it extends (e.g. presence helpers onStateManager, notApiorAppdelegators). AppDaemon's broadself.*surface is a cautionary tale — diverge from its names and signatures where it chose poorly. - Convenience APIs must earn their place. Ask what boilerplate it saves, whether callers could write it trivially against the primitives, and whether the wrapper adds failure modes (races, weaker typing, untestable paths) the primitive doesn't have. If it saves a few lines and adds new failure modes, document the pattern instead.
- Every new
Apimethod is testable through the harness. Define how it behaves onRecordingApi(records anApiCallfor writes, reads a harness seed surface for reads) and add the seed API if needed (harness mechanics:.claude/rules/test-conventions.md). Never stub it withNotImplementedError. - HA-facing design follows Home Assistant / HACS conventions (Integration Quality Scale, current core idioms). A divergence needs a stated, strong reason.
Internal Documentation
Applies to CLAUDE.md, .claude/, design/, and code comments — not the user-facing docs site, where stating defaults and values is the point.
- State decisions; point to code for values. Write rules, rationale, and which file, component, token, or function is canonical. Don't restate a value, class list, signature, or output format that lives in code — a copied value drifts silently and nothing catches it. A pointer can't drift and steers the reader to reuse instead of reimplement.
- Exception: a command may carry the literal value it needs to be copy-pasteable. When that value is defined in code, name the source next to it (e.g.
-n 4—XDIST_WORKERSinnoxfile.py) so drift can be checked.
- Exception: a command may carry the literal value it needs to be copy-pasteable. When that value is defined in code, name the source next to it (e.g.
- A pointer names something that exists. When you move or rename a doc section, grep for references to it (including code comments) and update them.
App Pattern
class MyConfig(AppConfig):
model_config = SettingsConfigDict(env_prefix="my_")
setting_name: str = "default"
class MyApp(App[MyConfig]):
async def on_initialize(self):
await self.bus.on_state_change("light.kitchen", handler=self.on_light_change, name="kitchen_light")
await self.scheduler.run_in(self.my_task, 5)
async def on_light_change(self, event: RawStateChangeEvent):
passTesting
- Bugs: reproduce, write a failing test (RED), fix (GREEN), run the full file. Patterns for races, timeouts, and sentinel filtering are in
.claude/rules/regression-test-patterns.md(loads undertests/). - Test infrastructure, factories, and mock strategy:
.claude/rules/test-conventions.mdandtests/TESTING.md. - Run pytest with
-n 4, never-n auto. That matches CI (XDIST_WORKERSinnoxfile.py), and--dist loadscopemakes the worker count change which modules share a worker. - Run any test you fix or modify before committing — code inspection misses marker filtering, warning config, fixture scoping, and async timing.
Before Shipping
- Core changes (
src/hassette/core/,src/hassette/resources/,src/hassette/types/enums.py): the local gate is the unit/integration suite, lint + pyright, and the schema-freshness check. CI runsnox -s system_with_coverageandnox -s e2eon every push, so don't run those locally unless you're debugging (uv run nox -s systemneeds Docker). - Docs changes (
docs/pages/): rundoc-persona-reviewanddoc-accuracy-reviewon the touched pages. See.claude/rules/doc-rules.md. - UI changes: use the demo stack (
mise run demo) for visual QA, and capture screenshots only viascripts/capture_screenshots.py, never raw Playwright. Details in.claude/rules/demo-and-screenshots.md.
GitHub
- Issues: read
.claude/reference/github-workflow.mdbefore filing — titles, required type/area/size labels, topic labels, body sections. For pre-existing findings from/mine-clean-codeor review runs, also read.claude/reference/clean-code-findings.md. - PR checks: a red
file-sizesorduplicate-codecheck always means this PR regressed something; fix it in-PR. Details in the same reference file. - Closing issues: repeat the keyword for each issue (
closes #1, closes #2);Closes #1, #2only closes the first. - CodeRabbit auto-review is disabled. Once the PR is created and marked ready, comment
@coderabbitai review. - Roadmap:
design/roadmap.mdis the source of truth for Now/Next and forced orderings — read it when asked what to pick up next. Each initiative is a GitHub milestone whose description holds its Done-when. A follow-up issue joins an initiative's milestone only if the Done-when fails without it; otherwise leave it milestone-less (epic:*/topic:*labels are fine).
Changelog
Don't edit CHANGELOG.md by hand in feature PRs — release-please generates it from conventional commits, and the squash-merged PR title becomes the changelog line. The only hand edit is curating CHANGELOG.md on a release-please PR's branch; changelog-quality.md covers that procedure, including why that PR's body is never touched.
feat,fix,perf,refactor, anddocsappear in the changelog; usechore:for internal work (design/,.claude/, research, tooling),ci:, ortest:.docs:is only for user-facing docs.- PR titles describe the user-visible outcome, not implementation details or a bundle of fixes.
- A breaking change (
feat!:etc.) needs exactly oneBREAKING CHANGE:footer at the end of the PR body — release-please drops every footer after the first.
Read .claude/rules/changelog-quality.md before creating a PR with a breaking change or reviewing a release-please PR.
Design Artifacts
Internal design documents live in design/, not docs/ (the published docs site). design/context.md is the canonical UI design direction. See design/README.md for what goes where.
Code Style
- Line length 120, type hints everywhere, Google-style docstrings.
- Ruff + Pyright (commands above).
- No
from __future__ import annotations. - No blanket
# type: ignore— use# pyright: ignore[reportXxx]. - Mixins declare the attributes they expect from their host as class-level annotations. Don't add a runtime
Protocolto validate the host; aTYPE_CHECKING-only one is fine when annotations can't type it (see_LifecycleHostPinresources/mixins.py). - User-facing API methods keep their full docstrings even when siblings repeat them (scheduler
run_*, buson_*delegates) — users read the method they call. Don't flag that as duplication; duplicated bodies are still fair game.
In this repository10 other files it gives its agents.
- .github/copilot-instructions.mdCopilot instructions
- doc-accuracy-reviewSkill
- doc-coverage-reviewSkill
- doc-persona-reviewSkill
- docsSkill
- ha-version-bumpSkill
- review-autofixesSkill
- triage-autofix-issuesSkill
- triage-medium-issuesSkill
- ui-qaSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

