Home Assistant
Running Home Assistant Core and frontend development servers through dot, pitchfork and Herdr.
I work on Home Assistant Core and the frontend most days. dot homeassistant (dot ha) sets up each repository, then runs its dev server as a pitchfork daemon so it gets a stable HTTPS URL from pitchfork’s proxy. Under Herdr, each command opens its own tab in the [HA] Core or [HA] Frontend workspace.
The short hacdev, hafdev* and hadev* functions in .zshrc just call dot ha. Repository paths, workspaces, daemon names, URLs and serve targets live in the private ~/.config/dot/homeassistant.yml, and the pitchfork config lives in the private overlay too, so it is copied below with real URLs replaced.
Commands
| Command | Alias |
|---|---|
dot homeassistant |
dot ha |
dot homeassistant core |
dot ha c |
dot homeassistant frontend |
dot ha f |
| Case | Command | Shortcut |
|---|---|---|
| What is running, and where | dot ha status [target...] |
hadevstatus, hacdevstatus, hafdevstatus, hafdevservestatus, hafdevsuitesstatus |
| Logs | dot ha logs <target> [--follow] [--lines <n>] |
hacdevlogs, hafdevlogs, hafdevservelogs, hafdevdesignlogs, hafdevdemologs, hafdeve2eapplogs |
| Core with the local frontend build | dot ha core dev [--background] |
hacdev, hacdevbg |
Core, rebased onto upstream/dev first |
dot ha core dev --latest [--background] |
hacdevlatest, hacdevlatestbg |
| Core setup (Git, venv, bootstrap) | dot ha core setup [--latest] |
Run by hacdev* |
| Frontend watch build alone | dot ha frontend dev [--background] [--attach] |
hafdev, hafdevbg |
| Frontend against another Core | dot ha frontend serve [<target or URL>] [--background] |
hafdevserve |
| Frontend against prod | dot ha frontend serve prod [--background] |
hafdevserveprod, hafdevserveprodbg |
| Frontend through Home Assistant Link | dot ha frontend serve link [--background] |
hafdevserveprodnc, hafdevserveprodncbg |
| Gallery | dot ha frontend gallery [--background] |
hafdevdesign, hafdevdesignbg |
| Demo | dot ha frontend demo [--background] |
hafdevdemo, hafdevdemobg |
| E2E test app | dot ha frontend e2e [--background] |
hafdeve2eapp, hafdeve2eappbg |
| Production build | dot ha frontend build |
hafbuild |
| E2E tests | dot ha frontend test-e2e [app|demo|gallery] |
None |
| Core and frontend in Herdr tabs | dot ha dev [--background] |
hadev, hadevbg |
| Stop servers (all by default) | dot ha stop [target...] |
hadevstop, hacdevstop, hafdevstop, hafdevservestop, hafdevdesignstop, hafdevdemostop, hafdeve2eappstop, hafdevsuitesstop |
| Target | Daemon | URL |
|---|---|---|
core |
ha-core/dev |
https://dev.ha-core.localhost |
build |
ha-frontend/build |
Through Core’s URL |
serve |
ha-frontend/serve |
https://serve.ha-frontend.localhost |
prod |
ha-core/prod |
https://prod.ha-core.localhost |
gallery, demo, e2e |
The frontend’s own background mode | Printed when started |
Port 443 already reaches pitchfork’s proxy (listening on 8443), so the URLs need no port. The proxy builds each hostname from the daemon name and namespace. Its auto-start is off, so a stopped daemon’s URL shows pitchfork’s “No daemon found” page rather than starting it; only the commands start daemons.
How the commands behave
- Interactive runs go through
dot status-run, which pins a header with the state, URL and elapsed time, and keeps the terminal title in step. - Each runs its setup first:
dot ha core setupfor Core (Git, virtual environment,script/bootstrap),script/bootstrapfor the frontend. - If the daemon is already running, it asks before restarting it.
dot ha dev’s Frontend tab follows a running build instead. --backgroundstarts the daemon and returns once it’s ready; otherwise it follows the logs, and Ctrl+C stops it.- Every
dot ha frontendcommand takes the frontend’s build lock, so it asks to stop thebuildandservedaemons first. Lint, format, type checks and unit tests don’t take the lock and stay plainpnpm.
Core behind the proxy
The proxy sets Host to localhost:<port> and adds X-Forwarded-* headers. Core rejects those unless it trusts the proxy. In the dev Core, turn on Trust X-Forwarded-For and add 127.0.0.1/32 and ::1/128 to Trusted proxies, under Settings > System > Network > Reverse proxy.
Serving against a remote Core also needs https://serve.ha-frontend.localhost in that instance’s CORS allowed origins, on the same page.
The browser blocks an HTTPS page from calling a plain HTTP Core, so the login works but the frontend can’t connect afterwards. ha-core/prod fronts the prod instance on an HTTPS URL with dot http-forward, which drops the proxy’s X-Forwarded-* headers so the remote Core accepts the requests without trusting this machine as a proxy. dot ha f serve prod starts it before serve.
Core only registers the frontend routes if hass_frontend exists when it starts, and a fresh build briefly removes it. ha-core/dev depends on ha-frontend/build, so starting Core starts the build too and waits for it to be ready.
Agents
Agents use the same dot ha commands. Under an agent, dot ha skips setup, Herdr tabs and prompts: it reuses a running daemon, runs suites in the background, and fails with a message instead of stopping a conflicting daemon. dot ha core setup and dot ha logs --follow refuse to run. The Home Assistant workspace AGENTS.md tells agents not to run hass, pnpm dev, pnpm dev:serve or pnpm build directly, and to ask before stopping or swapping a running daemon.
Pitchfork config
~/.config/pitchfork/config.toml enables the proxy and registers both repositories as namespaces:
[daemons]
[settings.proxy]
auto_start = false
enable = true
https = true
port = 8443
[namespaces.ha-core]
dir = "/home/<user>/repos/home-assistant/core"
config = ["/home/<user>/.config/dotfiles-private/pitchfork/.config/pitchfork/home-assistant/core.toml"]
[namespaces.ha-frontend]
dir = "/home/<user>/repos/home-assistant/frontend"
config = ["/home/<user>/.config/dotfiles-private/pitchfork/.config/pitchfork/home-assistant/frontend.toml"]
home-assistant/core.toml:
[daemons.dev]
run = "exec .venv/bin/hass -c config"
depends = ["ha-frontend/build"]
port = 8123
ready_port = { port = 8123, timeout = "5m" }
retry = true
stop_signal = { signal = "SIGTERM", timeout = "60s" }
[daemons.prod]
run = "exec dot http-forward --port 8126 --target http://homeassistant.local:8123"
port = 8126
ready_port = 8126
home-assistant/frontend.toml:
[daemons.build]
run = "exec pnpm dev --fetch-translations"
ready_output = { pattern = "Build done", timeout = "10m" }
[daemons.serve]
run = """
args="${XDG_STATE_HOME:-$HOME/.local/state}/dot/homeassistant/serve.args"
eval "set -- $(cat "$args" 2>/dev/null)"
exec pnpm dev:serve --fetch-translations "$@"
"""
port = 8124
ready_port = { port = 8124, timeout = "10m" }
dot config
~/.config/dot/homeassistant.yml:
core:
directory: ~/repos/home-assistant/core
workspace: "[HA] Core"
daemon: ha-core/dev
url: https://dev.ha-core.localhost
frontend:
directory: ~/repos/home-assistant/frontend
workspace: "[HA] Frontend"
build: ha-frontend/build
serve:
daemon: ha-frontend/serve
url: https://serve.ha-frontend.localhost
targets:
prod:
title: Prod
url: https://prod.ha-core.localhost
daemon: ha-core/prod
link:
title: Link
url: https://<instance>.ui.nabu.casa