Third and final config fix — verified against Z2M's own validator this time, not guessed.
Root cause (definitive)
Z2M refused to start with a generic "configuration is invalid" message. Running Z2M's own settings.validate() in the image gave the real error:
THROW: unknown scalar tag !<!secret> (17:13)
password: !secret mqtt_password
Z2M 2.x does not register a !secret YAML tag. Its secret mechanism is parseValueRef — a regex !<file> <key> run against the string value of mqtt.password (etc.) in read(). So the reference must be a quoted string:
password:"!secret mqtt_password"
Changes
Quote the !secret ref in the ConfigMap; restore the init container rendering /app/data/secret.yaml (the interim ZIGBEE2MQTT_CONFIG_* env-override was reverted — it's only applied in write()/onboarding, not on a plain read, so it left mqtt.password empty; confirmed in-image).
Verified end-to-end (real rendered ConfigMap, in the running image)
VALIDATE_ERRORS: []
mqtt.password: <resolved from secret.yaml>
devices: 31
Cutover state
✅ OpenBao backup, ExternalSecret synced
✅ZHA re-enabled — your Zigbee is working now (I rolled it back after the earlier failed starts; Z2M never touched the coordinator)
⏳ After merge: I clear the stale PVC config once, Z2M starts & validates against the live coordinator (ZHA still holds it, so it'll reach connect-stage — proving config good), then the final flip (disable ZHA → Z2M restores).
Third and final config fix — **verified against Z2M's own validator this time**, not guessed.
## Root cause (definitive)
Z2M refused to start with a generic "configuration is invalid" message. Running Z2M's own `settings.validate()` in the image gave the real error:
```
THROW: unknown scalar tag !<!secret> (17:13)
password: !secret mqtt_password
```
Z2M 2.x does **not** register a `!secret` YAML *tag*. Its secret mechanism is `parseValueRef` — a regex `!<file> <key>` run against the **string value** of `mqtt.password` (etc.) in `read()`. So the reference must be a **quoted string**:
```yaml
password: "!secret mqtt_password"
```
## Changes
- Quote the `!secret` ref in the ConfigMap; restore the init container rendering `/app/data/secret.yaml` (the interim `ZIGBEE2MQTT_CONFIG_*` env-override was reverted — it's only applied in `write()`/onboarding, **not** on a plain read, so it left `mqtt.password` empty; confirmed in-image).
## Verified end-to-end (real rendered ConfigMap, in the running image)
```
VALIDATE_ERRORS: []
mqtt.password: <resolved from secret.yaml>
devices: 31
```
## Cutover state
- ✅ OpenBao backup, ExternalSecret synced
- ✅ **ZHA re-enabled** — your Zigbee is working now (I rolled it back after the earlier failed starts; Z2M never touched the coordinator)
- ⏳ After merge: I clear the stale PVC config once, Z2M starts & validates against the live coordinator (ZHA still holds it, so it'll reach connect-stage — proving config good), then the final flip (disable ZHA → Z2M restores).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Root cause of Z2M refusing to start, verified against Z2M's own validator: the
MQTT password was written as a bare YAML tag `password: !secret mqtt_password`,
and Z2M 2.x does NOT register a `!secret` YAML tag — js-yaml throws
"unknown scalar tag !<!secret>", which surfaces as the generic
"configuration is invalid (use yaml-validator)" message.
Z2M's secret mechanism is parseValueRef (settings.ts read()): it matches the
regex `!<file> <key>` against the STRING VALUE of mqtt.user/password/server,
advanced.network_key, frontend.auth_token. So the reference must be a QUOTED
string, not a tag:
password: "!secret mqtt_password"
Reverted the interim env-override attempt: ZIGBEE2MQTT_CONFIG_* is applied only
in write()/onboarding, not on a plain read of an existing config, so it did NOT
set mqtt.password (confirmed: mqtt.password was empty). The quoted-!secret +
init-rendered /app/data/secret.yaml path is the one Z2M actually resolves on
read.
Verified end-to-end on the REAL rendered ConfigMap via `settings.validate()` in
the running image: VALIDATE_ERRORS: [], mqtt.password resolves from secret.yaml,
31 devices parsed.
Post-merge: the PVC still holds the pre-fix seeded configuration.yaml (init only
seeds when absent), so it must be cleared once so it re-seeds from this fixed
ConfigMap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Third and final config fix — verified against Z2M's own validator this time, not guessed.
Root cause (definitive)
Z2M refused to start with a generic "configuration is invalid" message. Running Z2M's own
settings.validate()in the image gave the real error:Z2M 2.x does not register a
!secretYAML tag. Its secret mechanism isparseValueRef— a regex!<file> <key>run against the string value ofmqtt.password(etc.) inread(). So the reference must be a quoted string:Changes
!secretref in the ConfigMap; restore the init container rendering/app/data/secret.yaml(the interimZIGBEE2MQTT_CONFIG_*env-override was reverted — it's only applied inwrite()/onboarding, not on a plain read, so it leftmqtt.passwordempty; confirmed in-image).Verified end-to-end (real rendered ConfigMap, in the running image)
Cutover state
🤖 Generated with Claude Code
Root cause of Z2M refusing to start, verified against Z2M's own validator: the MQTT password was written as a bare YAML tag `password: !secret mqtt_password`, and Z2M 2.x does NOT register a `!secret` YAML tag — js-yaml throws "unknown scalar tag !<!secret>", which surfaces as the generic "configuration is invalid (use yaml-validator)" message. Z2M's secret mechanism is parseValueRef (settings.ts read()): it matches the regex `!<file> <key>` against the STRING VALUE of mqtt.user/password/server, advanced.network_key, frontend.auth_token. So the reference must be a QUOTED string, not a tag: password: "!secret mqtt_password" Reverted the interim env-override attempt: ZIGBEE2MQTT_CONFIG_* is applied only in write()/onboarding, not on a plain read of an existing config, so it did NOT set mqtt.password (confirmed: mqtt.password was empty). The quoted-!secret + init-rendered /app/data/secret.yaml path is the one Z2M actually resolves on read. Verified end-to-end on the REAL rendered ConfigMap via `settings.validate()` in the running image: VALIDATE_ERRORS: [], mqtt.password resolves from secret.yaml, 31 devices parsed. Post-merge: the PVC still holds the pre-fix seeded configuration.yaml (init only seeds when absent), so it must be cleared once so it re-seeds from this fixed ConfigMap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>