doctor: detect pool-account identity drift vs live ~/.claude.json#353
Merged
Conversation
When a user re-installs Claude Code (or switches the active account inside
CC) after `dario accounts add`, the stored deviceId/accountUuid snapshot in
`~/.dario/accounts/<alias>.json` no longer matches what the proxy reads
live from `~/.claude.json` per request. Anthropic cross-validates the OAuth
bearer against `metadata.user_id` (built from the live deviceId) and 401s
with `authentication_error` on non-Haiku models when they disagree — Haiku
is more permissive and may succeed despite the mismatch, which makes the
failure mode look intermittent and account-tier-shaped even though it's an
identity-staleness bug.
Add a new "Identity" row to `dario doctor` that compares each pool
account's stored snapshot against live `~/.claude.json`:
- pool aligned → [ OK ] N/N pool accounts match
- pool drifted → [WARN] names the alias(es) + which field
- no ~/.claude.json → [INFO] warns proxy will hit Extra Usage
- single-account mode → [INFO] notes drift only surfaces as 401
(no stored baseline to compare;
opt-in network probe is follow-up)
The comparison is factored into a pure exported function
`checkIdentityDrift({live, poolAccounts})` so the branches are unit-
testable without filesystem fixtures. New test/doctor-identity-drift.mjs
covers all branches; existing test/doctor-formatter.mjs unchanged.
Zero new runtime dependencies. Zero network calls in the always-on path
(pool drift is purely a file-compare). detectClaudeIdentity is promoted
from module-private to exported in src/accounts.ts so doctor can call it.
Merged
askalf
added a commit
that referenced
this pull request
May 21, 2026
Ships the dario doctor Identity drift check from #353. New "Identity" row detects when a pool account's stored {deviceId, accountUuid} snapshot has drifted from the live ~/.claude.json — the silent-401 footgun where Haiku calls still succeed but Sonnet/Opus return authentication_error on a mismatched bearer vs metadata.user_id. Co-authored-by: askalf <263217947+askalf@users.noreply.github.com>
askalf
added a commit
that referenced
this pull request
May 23, 2026
Pre-fix: narrow file-list filter meant only PRs explicitly modifying test/compat.mjs or src/proxy.ts triggered compat. Result: 5 release PRs (v4.8.5 .. v4.8.9 maxTested bumps) shipped with zero compat coverage despite being EXACTLY what compat-test is supposed to validate. PR #353 (doctor identity-drift check) also missed coverage even though it touches the same auth surface compat exercises. Audit on 2026-05-23 against 15 merged dario PRs in the prior 7 days showed only 2 of 15 triggered compat (#344 + #347, both touched the compat workflow itself). Broadens to: - src/** — any source change validates against the live CC version - package.json — version bumps (maxTested + dario version) - (existing) scripts/capture-and-bake.mjs, test/compat.mjs, the workflow file itself. Workflow + docs only changes still skip compat (no need to compat- test a README change). The 80% historical failure rate was a pre-secret artifact — ANTHROPIC_COMPAT_API_KEY secret was added 2026-05-20 and verified clean on master 2026-05-23 ~13:00 UTC. So broadening here adds COVERAGE without adding NOISE. Co-authored-by: askalf <263217947+askalf@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Add an "Identity" row to
dario doctorthat compares each pool account's stored{deviceId, accountUuid}snapshot against the live~/.claude.jsonand warns on drift.Why
When a user re-installs Claude Code, switches the active account inside CC, or manually edits
~/.claude.jsonafterdario accounts add, the stored snapshot in~/.dario/accounts/<alias>.jsonno longer matches what the proxy reads live from~/.claude.jsonper request. Anthropic cross-validates the OAuth bearer againstmetadata.user_id(built from the live deviceId) and 401s withauthentication_erroron non-Haiku models when they disagree.The catch: Haiku is more permissive and may succeed despite the mismatch, which makes the failure mode look intermittent and account-tier-shaped. A real incident from yesterday burned ~2 hours misdiagnosing it as rate-limit depletion before tracing it to a one-line userID drift in a host-mounted
.claude.json.dario doctorwould have surfaced it in one second.How
checkIdentityDrift({live, poolAccounts}): Check[]insrc/doctor.tsso each branch is unit-testable without filesystem fixtures.runChecks()between Pool and Backends: reads live identity viadetectClaudeIdentity(), loads pool accounts, calls the pure function, pushes the result. Defensively wrapped — a throw becomes[WARN] check failed: …like every other doctor section.detectClaudeIdentityfrom module-private toexportinsrc/accounts.tsso doctor can reuse it. No call-site changes.Branches the check covers
~/.claude.jsonmissing or emptyinfometadata.user_id→ routes to Extra Usage billing instead of Max planinfookN/N pool accounts match ~/.claude.json (userID=…)warndeviceId/accountUuid/both) + recommendsdario accounts add <alias>to refreshTests
test/doctor-identity-drift.mjs— 30 assertions across all 7 branches above. Registered intest:serialand auto-picked-up bytest/all.test.mjsparallel driver.Full suite: 77/77 green locally (
node --test --test-concurrency=8 test/all.test.mjs).Out of scope (follow-up)
Single-account mode can't detect bearer-vs-
.claude.jsondrift locally because dario stores no baseline alongside~/.dario/credentials.json— the proxy reads identity live per-request. Two future-paths to cover it:dario doctor --identitynetwork probe — one Sonnet call toapi.anthropic.comwith bearer +metadata.user_idfrom live.claude.json. 401 = mismatch. Parallels existing--probe/--usageopt-ins.dario logincould writeuserID/accountUuidintocredentials.jsonalongside the bearer; doctor then compares against live just like pool mode.Either of those would close the single-account hole. Kept out of this PR to keep scope tight to the zero-network-cost detection.
Risk
Additive only — no behavior change to login, refresh, proxy, or any existing check. Identity row is a new line in
dario doctoroutput; no API surface changes. Worst case: the new check throws and the I/O wrapper converts it to[WARN] Identity check failed: …, indistinguishable from any other defensive doctor row.