Found by testing ha-token mode against live Home Assistant before wiring the CronJobs into GitOps — the mode as merged in #318 cannot actually rotate an existing token.
The bug
Home Assistant refuses to mint a second long-lived access token whose client_name already exists. Make-before-break necessarily mints the replacement while the outgoing token — same base name — is still present, so every real rotation fails at mint:
same-name mint -> FAIL {'code':'unknown_error','message':'Unknown error'}
unique-name mint -> SUCCESS
Directly observed against HA 2026.7.2. The ad-hoc test during #318 only passed because it minted a new name ("Config Sync"), never exercising the collision.
The failure was safe — mint fails before store or revoke, so the old token stayed valid and stored (a no-op, verified: the stored token fingerprint was unchanged after the failed run). But nothing would ever rotate.
The fix
Mint under <base> [<6 hex>]. Correctness doesn't depend on the name — the outgoing token is identified by its JWT iss, never by name — so a random tag costs nothing, and it also avoids colliding with an orphan from a prior failed run. HA_CLIENT_NAME stays the recognizable base ("ServiceMonitor", "Shelly Manager", …); the stored token just carries a rotating suffix.
Verified end-to-end
Against live HA on a throwaway token (zero blast radius — no live consumer touched):
unique-name mint succeeds where a reused base name fails
the outgoing token is found by its iss and confirmed an LLAT
store patches OpenBao
the old token is revoked and stops authenticating (auth_invalid)
The binary (#318) is already merged and its image published. This is the corrected binary; once merged, CI republishes the image and the Home Assistant CronJob PR can reference the new digest. That CronJob PR also has to add a Reloader annotation for shelly-manager-llat — testing turned up that shelly-manager reads it as a startup env var but its only Reloader annotation points at a different secret, so a rotation would not restart it.
Found by testing `ha-token` mode against live Home Assistant **before** wiring the CronJobs into GitOps — the mode as merged in #318 cannot actually rotate an existing token.
## The bug
Home Assistant refuses to mint a second long-lived access token whose `client_name` already exists. Make-before-break necessarily mints the replacement **while the outgoing token — same base name — is still present**, so every real rotation fails at mint:
```
same-name mint -> FAIL {'code':'unknown_error','message':'Unknown error'}
unique-name mint -> SUCCESS
```
Directly observed against HA 2026.7.2. The ad-hoc test during #318 only passed because it minted a *new* name ("Config Sync"), never exercising the collision.
The failure was **safe** — mint fails before store or revoke, so the old token stayed valid and stored (a no-op, verified: the stored token fingerprint was unchanged after the failed run). But nothing would ever rotate.
## The fix
Mint under `<base> [<6 hex>]`. Correctness doesn't depend on the name — the outgoing token is identified by its JWT `iss`, never by name — so a random tag costs nothing, and it also avoids colliding with an orphan from a prior failed run. `HA_CLIENT_NAME` stays the recognizable base ("ServiceMonitor", "Shelly Manager", …); the stored token just carries a rotating suffix.
## Verified end-to-end
Against live HA on a **throwaway** token (zero blast radius — no live consumer touched):
- unique-name mint succeeds where a reused base name fails
- the outgoing token is found by its `iss` and confirmed an LLAT
- store patches OpenBao
- the old token is revoked and stops authenticating (`auth_invalid`)
`clippy -D warnings` clean; 12 tests pass (adds `random_hex` length/charset/uniqueness).
## Why a separate PR
The binary (#318) is already merged and its image published. This is the corrected binary; once merged, CI republishes the image and the Home Assistant CronJob PR can reference the new digest. That CronJob PR also has to add a Reloader annotation for `shelly-manager-llat` — testing turned up that shelly-manager reads it as a startup env var but its only Reloader annotation points at a *different* secret, so a rotation would not restart it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Found by testing ha-token mode against live HA before wiring up the CronJobs.
Home Assistant refuses to mint a second long-lived access token whose
client_name already exists — and make-before-break necessarily mints while the
outgoing token (same base name) is still present. So every real rotation would
fail at the mint step with "Unknown error", exactly as observed:
same-name mint -> FAIL {'code':'unknown_error'}
unique-name mint -> SUCCESS
The earlier ad-hoc test only passed because it minted a NEW name
("Config Sync") rather than rotating an existing one.
Fix: mint under "<base> [<6 hex>]". Correctness does not depend on the name —
the outgoing token is identified by its JWT `iss`, never by name — so a random
tag is free, and it also avoids colliding with an orphan left by a prior failed
run. The failure mode was safe throughout: mint fails before store or revoke,
so the old token stays valid and stored (a no-op, not an outage).
Verified end-to-end against live HA on a throwaway token: unique mint succeeds,
the outgoing token is found by iss, store patches OpenBao, and the old token is
revoked and stops authenticating.
clippy -D warnings clean; 12 tests pass.
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.
Found by testing
ha-tokenmode against live Home Assistant before wiring the CronJobs into GitOps — the mode as merged in #318 cannot actually rotate an existing token.The bug
Home Assistant refuses to mint a second long-lived access token whose
client_namealready exists. Make-before-break necessarily mints the replacement while the outgoing token — same base name — is still present, so every real rotation fails at mint:Directly observed against HA 2026.7.2. The ad-hoc test during #318 only passed because it minted a new name ("Config Sync"), never exercising the collision.
The failure was safe — mint fails before store or revoke, so the old token stayed valid and stored (a no-op, verified: the stored token fingerprint was unchanged after the failed run). But nothing would ever rotate.
The fix
Mint under
<base> [<6 hex>]. Correctness doesn't depend on the name — the outgoing token is identified by its JWTiss, never by name — so a random tag costs nothing, and it also avoids colliding with an orphan from a prior failed run.HA_CLIENT_NAMEstays the recognizable base ("ServiceMonitor", "Shelly Manager", …); the stored token just carries a rotating suffix.Verified end-to-end
Against live HA on a throwaway token (zero blast radius — no live consumer touched):
issand confirmed an LLATauth_invalid)clippy -D warningsclean; 12 tests pass (addsrandom_hexlength/charset/uniqueness).Why a separate PR
The binary (#318) is already merged and its image published. This is the corrected binary; once merged, CI republishes the image and the Home Assistant CronJob PR can reference the new digest. That CronJob PR also has to add a Reloader annotation for
shelly-manager-llat— testing turned up that shelly-manager reads it as a startup env var but its only Reloader annotation points at a different secret, so a rotation would not restart it.🤖 Generated with Claude Code
Found by testing ha-token mode against live HA before wiring up the CronJobs. Home Assistant refuses to mint a second long-lived access token whose client_name already exists — and make-before-break necessarily mints while the outgoing token (same base name) is still present. So every real rotation would fail at the mint step with "Unknown error", exactly as observed: same-name mint -> FAIL {'code':'unknown_error'} unique-name mint -> SUCCESS The earlier ad-hoc test only passed because it minted a NEW name ("Config Sync") rather than rotating an existing one. Fix: mint under "<base> [<6 hex>]". Correctness does not depend on the name — the outgoing token is identified by its JWT `iss`, never by name — so a random tag is free, and it also avoids colliding with an orphan left by a prior failed run. The failure mode was safe throughout: mint fails before store or revoke, so the old token stays valid and stored (a no-op, not an outage). Verified end-to-end against live HA on a throwaway token: unique mint succeeds, the outgoing token is found by iss, store patches OpenBao, and the old token is revoked and stops authenticating. clippy -D warnings clean; 12 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>