ADR-0007: ESPHome configs — builder workspace (config/) vs. tracked mirror (devices/)¶
- Status: Accepted
- Date: 2026-07-20
Context¶
ESPHome device configs are edited in the ESPHome Device Builder (the esphome stack), which
mounts stacks/esphome/config/ as its working directory. That directory can't be version-controlled
as-is:
- The builder makes it its own git repository (
config/.git) — and recreates that repo from inside its container, which cannot see this repo's.git. So the nested repo keeps coming back, and git refuses to track files inside a nested repo:config/simply can't be committed here. - It also holds a ~10 GB build cache (
.esphome/), builder state (.device-builder*), andsecrets.yamlwith real credentials.
So stacks/esphome/config/ is gitignored, and the device YAMLs are mirrored into a tracked
stacks/esphome/devices/. That mirror was maintained by hand and had drifted both ways —
a live device (bathroom-full-speakers) missing from the builder, other files missing from the
mirror.
Secrets were inconsistent too: WiFi used !secret, but several device YAMLs hard-coded API
encryption keys, OTA passwords, and fallback-hotspot AP passwords inline, which then landed in the
tracked mirror and git history.
Decision¶
- Two directories, one source of truth.
config/is the builder's private workspace (gitignored);devices/is a one-way export of just the device YAMLs and is the versioned copy. - Automated export.
stacks/esphome/sync-devices.shexportsconfig/ → devices/(rsync; excludes secrets/cache/state; deletion is opt-in via--mirror). A pre-commit hook (tools/hooks/esphome-sync.sh, wired into.githooks/pre-commit) runs it on every commit and stagesdevices/, so the two can't drift. It is additive and a no-op on clones without the builder. - No inline secrets. All API keys / OTA passwords / AP passwords are
!secret, sourced from the gitignoredconfig/secrets.yaml; a check that every!secretreference resolves guards against build breakage.
Consequences¶
- Edit devices in the builder (
config/); the tracked mirror updates itself at commit time. Never hand-editdevices/. - Retiring a device needs a deliberate
sync-devices.sh --mirror(the hook never deletes). - Fresh clones / other hosts have no
config/; the hook no-ops there and the committeddevices/is the reference. - Pre-existing inline-secret values were scrubbed from history (
git-filter-repo --replace-text, dates preserved) alongside this ADR. Rotate the affected keys if that history was ever exposed.