config.Save is a full-file overwrite — os.WriteFile with no temp-file+rename (internal/config/config.go:120-133) — and several code paths do independent load→mutate→save cycles on the whole struct:
- token refresh persisting rotated tokens (internal/auth/refresh.go:131,
SaveTokens)
- the auto update checker (cmd/root.go:103-131, saves twice per check)
- anonymous token minting (internal/api/client.go:344-367)
- analytics distinct-id creation on first run (internal/analytics/analytics.go:52-55)
The refresh flow rotates refresh tokens (the oauth tests expect a new refresh_token back), which makes the race destructive:
- A long-lived
codag mcp serve session refreshes: config now holds AT-new/RT-2; RT-1 is consumed server-side.
- A parallel
codag <anything> invocation loaded config before that refresh and saves after it (update checker, anon-token mint, or distinct-id write) — writing AT-old/RT-1 back over the rotated pair.
- Next 401 → refresh fires with the revoked RT-1 →
ErrUnauthenticated → user is told to codag auth login again.
From the user's side this is "codag randomly logs me out", which is painful to attribute precisely because the failing process isn't the one that caused it. The MCP path makes the overlap window realistic: mcp serve runs for hours while the user also uses the CLI directly.
Related smaller issues with the same root cause:
- Non-atomic writes mean a crash mid-
Save leaves a corrupt config.json; Load then errors and auth paths fail until the file is deleted by hand.
- N concurrent MCP tool calls on a fresh install each mint an anonymous token (internal/api/client.go:344 has no single-flight) — the auth refresher solves exactly this stampede for OAuth (internal/auth/refresh.go:15-20), but the anon-token path doesn't reuse the pattern.
Suggested fix: atomic write (write temp file in the same dir, then rename) plus read-modify-write narrowing — re-Load immediately before Save and apply only the caller's delta, or guard cross-process with a lock file. Happy to discuss/PR.
config.Saveis a full-file overwrite —os.WriteFilewith no temp-file+rename (internal/config/config.go:120-133) — and several code paths do independent load→mutate→save cycles on the whole struct:SaveTokens)The refresh flow rotates refresh tokens (the oauth tests expect a new
refresh_tokenback), which makes the race destructive:codag mcp servesession refreshes: config now holds AT-new/RT-2; RT-1 is consumed server-side.codag <anything>invocation loaded config before that refresh and saves after it (update checker, anon-token mint, or distinct-id write) — writing AT-old/RT-1 back over the rotated pair.ErrUnauthenticated→ user is told tocodag auth loginagain.From the user's side this is "codag randomly logs me out", which is painful to attribute precisely because the failing process isn't the one that caused it. The MCP path makes the overlap window realistic:
mcp serveruns for hours while the user also uses the CLI directly.Related smaller issues with the same root cause:
Saveleaves a corrupt config.json;Loadthen errors and auth paths fail until the file is deleted by hand.Suggested fix: atomic write (write temp file in the same dir, then rename) plus read-modify-write narrowing — re-
Loadimmediately beforeSaveand apply only the caller's delta, or guard cross-process with a lock file. Happy to discuss/PR.