From a cross-model adversarial review (GPT-5.6 code pass + ChatGPT Pro invariant pass) of the switch/keychain/store surface, six findings were fixed in 0.24.3. The remaining findings are all crash-window / non-transactional-write hardening — real but NARROW (each needs a crash or I/O failure inside a small window), and they share the same class as every file-swap switcher. Tracking them here rather than force-fitting a fix.
Crash-window / transactionality
- Multi-resource apply is not crash-atomic (
src/adapters/claude.rs apply): a SIGKILL/power-loss between the credential-file write, the Keychain write, and the .claude.json write leaves the machine with A's token and B's identity. No durable journal to finish/rollback on next start. Fix direction: write+fsync a WAL (prior bytes + exact Keychain service) before the first mutation; recover on startup.
- Multi-blob profile save is not transactional (
src/store.rs:save): a crash between the credentials blob and the oauth_account blob leaves a profile with B's token and A's identity (each blob is valid JSON, so use accepts the mismatch). Fix direction: write a full generation to a staging dir, fsync, atomically flip a pointer/manifest.
- Rollback leaves a newly-created Keychain item (
src/adapters/claude.rs apply rollback): when no prior Keychain token was read, the None => true branch leaves A in the Keychain while the config is rolled back to B. Fix direction: tri-state prior (absent / read-failed / present); on rollback of a created item, delete that exact item; abort before writing if the prior read failed.
restore / lock
- A failed
restore strands the requested backup (src/commands.rs restore): restore loads backup A, then backs up the live B (making B newest), then apply(A) fails — a retry now sees B as newest and exits "already active", never retrying A. Fix direction: back up B provisionally, promote only after A applies.
- Interactive
login re-acquires the store lock best-effort (src/commands.rs login, the store.lock().ok() after the interactive sign-in): a concurrent swapdex use during the sign-in can interleave; on Busy at re-lock, login proceeds unlocked. Fix direction: a per-tool live-credential lock (distinct from the store lock, which must stay released across the interactive wait) held across sign-in; never best-effort at the end.
All are NARROW (crash/concurrency-timing preconditions). Prioritize the two write-atomicity ones (apply WAL, save staging) if any are done — they are the ones that can produce a token/identity mismatch a later use would silently apply.
From a cross-model adversarial review (GPT-5.6 code pass + ChatGPT Pro invariant pass) of the switch/keychain/store surface, six findings were fixed in 0.24.3. The remaining findings are all crash-window / non-transactional-write hardening — real but NARROW (each needs a crash or I/O failure inside a small window), and they share the same class as every file-swap switcher. Tracking them here rather than force-fitting a fix.
Crash-window / transactionality
src/adapters/claude.rsapply): a SIGKILL/power-loss between the credential-file write, the Keychain write, and the.claude.jsonwrite leaves the machine with A's token and B's identity. No durable journal to finish/rollback on next start. Fix direction: write+fsync a WAL (prior bytes + exact Keychain service) before the first mutation; recover on startup.src/store.rs:save): a crash between thecredentialsblob and theoauth_accountblob leaves a profile with B's token and A's identity (each blob is valid JSON, souseaccepts the mismatch). Fix direction: write a full generation to a staging dir, fsync, atomically flip a pointer/manifest.src/adapters/claude.rsapply rollback): when no prior Keychain token was read, theNone => truebranch leaves A in the Keychain while the config is rolled back to B. Fix direction: tri-state prior (absent / read-failed / present); on rollback of a created item, delete that exact item; abort before writing if the prior read failed.restore / lock
restorestrands the requested backup (src/commands.rsrestore): restore loads backup A, then backs up the live B (making B newest), then apply(A) fails — a retry now sees B as newest and exits "already active", never retrying A. Fix direction: back up B provisionally, promote only after A applies.loginre-acquires the store lock best-effort (src/commands.rslogin, thestore.lock().ok()after the interactive sign-in): a concurrentswapdex useduring the sign-in can interleave; onBusyat re-lock, login proceeds unlocked. Fix direction: a per-tool live-credential lock (distinct from the store lock, which must stay released across the interactive wait) held across sign-in; never best-effort at the end.All are NARROW (crash/concurrency-timing preconditions). Prioritize the two write-atomicity ones (apply WAL, save staging) if any are done — they are the ones that can produce a token/identity mismatch a later
usewould silently apply.