Symptom
Twice in two days, the rendered companion silently changed — the statusline showed Daffodil (a common duck) while the menagerie's active companion was still Cobalt (uncommon pikachu, wizard hat). The personality text stayed Cobalt's, which is what makes it read as "cursed": the pet's body changes but its voice doesn't.
status.json -> Daffodil duck
menagerie.json -> active: buddy -> Cobalt pikachu
Cause
status.json is a render cache derived from the menagerie, but nothing enforces that relationship. Any process that calls writeStatusState against the state dir can put an arbitrary companion in it, and the statusline will faithfully render whatever it finds.
In practice this happened when development/test processes ran ad-hoc commands against the real state dir (~/.config/claude/buddy-state) instead of a temporary one. Both times buddy_show restored it, because the menagerie was never corrupted — only the cache.
Why it matters beyond development
Any code path that hatches or renders a companion without going through the active-companion lookup can desync the cache. The user sees their pet replaced with no explanation and no way to tell whether the underlying companion was lost. It reads like data loss even though nothing was lost.
Proposed fix
Make the invariant explicit rather than assumed:
writeStatusState should verify the companion it is about to persist matches the menagerie's active companion, and refuse (or log loudly) on mismatch.
- On read, if
status.json disagrees with the menagerie's active companion, treat the menagerie as authoritative and rebuild the cache rather than rendering the stale/foreign entry.
- Add a regression test: write a foreign companion into
status.json, render, and assert the active companion is shown.
Optionally: a guard in the test/CI fixture helpers that refuses to write when the target resolves to the user's real state dir, so this class of accident cannot recur during development.
🤖 This content was generated with AI assistance using Claude Opus 5.
Symptom
Twice in two days, the rendered companion silently changed — the statusline showed
Daffodil(a common duck) while the menagerie's active companion was stillCobalt(uncommon pikachu, wizard hat). The personality text stayed Cobalt's, which is what makes it read as "cursed": the pet's body changes but its voice doesn't.Cause
status.jsonis a render cache derived from the menagerie, but nothing enforces that relationship. Any process that callswriteStatusStateagainst the state dir can put an arbitrary companion in it, and the statusline will faithfully render whatever it finds.In practice this happened when development/test processes ran ad-hoc commands against the real state dir (
~/.config/claude/buddy-state) instead of a temporary one. Both timesbuddy_showrestored it, because the menagerie was never corrupted — only the cache.Why it matters beyond development
Any code path that hatches or renders a companion without going through the active-companion lookup can desync the cache. The user sees their pet replaced with no explanation and no way to tell whether the underlying companion was lost. It reads like data loss even though nothing was lost.
Proposed fix
Make the invariant explicit rather than assumed:
writeStatusStateshould verify the companion it is about to persist matches the menagerie's active companion, and refuse (or log loudly) on mismatch.status.jsondisagrees with the menagerie's active companion, treat the menagerie as authoritative and rebuild the cache rather than rendering the stale/foreign entry.status.json, render, and assert the active companion is shown.Optionally: a guard in the test/CI fixture helpers that refuses to write when the target resolves to the user's real state dir, so this class of accident cannot recur during development.
🤖 This content was generated with AI assistance using Claude Opus 5.