Review on #1604: preferring `claudeAiOauth` was not enough. A record holding
only `mcpOAuth` entries still produced a token, because the fallback ran a
recursive `findOauthTokenSet()` over the whole record and `mcpOAuth`
per-plugin records carry their own `accessToken`. That imports a plugin
token as a Claude Code provider and recreates the 401 the change was meant
to avoid. It also short-circuited the plaintext-file fallback, so a usable
`~/.claude/.credentials.json` was ignored in favour of the plugin token.
Restrict both lookups to root-level token fields instead of reordering the
recursive search. Root-level excludes *any* nested container, so a future
sibling of `mcpOAuth` cannot reintroduce the hole, and the pre-`claudeAiOauth`
layout that kept tokens at the record root still resolves.
Extract `readOauthTokenSetFields()` out of `findOauthTokenSet()` in shared.ts
rather than duplicating the field list. `findOauthTokenSet()` keeps its
behaviour, so codex/grok/kimi/opencode/zcode are unaffected.
Apply the same restriction to the credentials-file path: Claude Code writes
that file with the identical record shape, mcpOAuth included, so the hazard
was present there too.
Adds the two regression tests from the review verbatim.
Refs #1601, #1567
Claude Code >= 2.1 writes its credential item under the current $USER and
leaves any pre-2.1 item (account "unknown") in place on the *same* service
name, `Claude Code-credentials`. `security find-generic-password -s <svc> -w`
with no `-a` matches an arbitrary one of them, so CCR read the stale legacy
item: the read exits 0, the JSON parses, and it carries only `mcpOAuth`.
`findOauthTokenSet()` then correctly returns undefined and the candidate is
silently dropped by the `status !== "missing"` filter in service.ts.
Resolve the item through tiered candidates, evaluated lazily so the normal
case stays at one `security` call:
1. the expected service name, derived the way Claude Code derives it
(`Claude Code${oauthSuffix}-credentials${configSuffix}`, where
configSuffix is `-${sha256(NFC(configDir)).slice(0, 8)}` once
CLAUDE_CONFIG_DIR / CLAUDE_SECURESTORAGE_CONFIG_DIR is set), read under
the current account;
2. `security dump-keychain` enumeration, newest-modified first, for items
under another account or a config dir this process cannot reconstruct.
Without `-d` that prints item metadata only: it never decrypts a
password and never prompts;
3. the previous accountless read, as a last resort.
Prefer an explicit top-level `claudeAiOauth` across every candidate and fall
back to the recursive search only if none has one. `mcpOAuth` holds
per-plugin OAuth records that also carry `accessToken`, and key order puts
it first, so a recursive match could import an MCP plugin token as an
Anthropic provider that then 401s on every request.
Surface diagnostics instead of swallowing them: capture `security` stderr
rather than discarding it via `stdio: ["ignore","pipe","ignore"]`, and
return a `locked` candidate naming the reason when login state exists but
yields no token, so it survives the `status !== "missing"` filter. Treat
errSecItemNotFound (exit 44) as absence rather than an error, so a machine
with no Claude Code login still reports `missing`.
Also fix the file fallback: continue past a credentials file that holds no
token instead of returning it, and check the secure-storage config dir,
which the env vars above can relocate away from ~/.claude.
Refs #1601, #1567
Public OpenCode imports wrote strict providerPlugins auth with
authorization: Bearer public, which overrode a later real Providers
api_key edit and caused upstream 401s. Put the public token on the
provider api key instead and skip the plugin override.
Fixes#1562
Provider imports exposed live context limits, but CCR discarded them and
fell back to stale static entries. Preserve physical and effective limits
so generated catalogs and model discovery advertise the provider-specific
usable window.