Skip to content

fix(models): guard Claude credential loader against malformed claudeAiOauth (#5473) - #5494

Merged
WillemJiang merged 2 commits into
bytedance:mainfrom
cyberspace-cs:fix/5473-claude-cred-json
Sep 17, 2026
Merged

WillemJiang merged 2 commits into
bytedance:mainfrom
cyberspace-cs:fix/5473-claude-cred-json

Conversation

@cyberspace-cs

Copy link
Copy Markdown
Contributor

Fixes #5473

Why

~/.claude/.credentials.json\ (or \) can contain a syntactically valid JSON \claudeAiOauth\ container that is not an object (e.g. a
ull\ left by a partial export, a raw string, an array, or a number). _extract_claude_code_credential\ called .get\ on that value, raising \AttributeError\ out of \ClaudeChatModel.model_post_init\ and aborting model construction - the opposite of the module's documented degrade-gracefully behaviour. The sibling Codex loader already guards the identical shape.

What changed

  • _extract_claude_code_credential\ now treats a non-object top-level payload and a non-object \claudeAiOauth\ value as "no credential from this source": it logs at debug level and returns \None, so \load_claude_code_credential()\ falls through to the next source and ultimately returns \None\ when every source is unusable.
  • Added regression tests: a parametrized case covering
    ull, string, list, int and a top-level array payload (all assert \load_claude_code_credential() is None), and a fallback test asserting an unusable override file falls back to the default ~/.claude/.credentials.json.

Surface area

  • Agents / LangGraph - model construction path (\ClaudeChatModel\ / credential loading); no prompt-layer data channels affected.

Bug fix verification

  • Test path that reproduces the bug: \�ackend/tests/test_credential_loader.py::test_load_claude_code_credential_ignores_malformed_oauth_container\
  • Did it go red on \main\ and green on this branch? yes - on \main\ (unfixed), all five malformed payloads raise \AttributeError: 'NoneType'/'str'/'list'/'int' object has no attribute 'get'; on this branch \load_claude_code_credential()\ returns \None\ for each and the full file passes (19 passed).

Validation

  • \cd backend && uv run pytest tests/test_credential_loader.py -q\ ? 19 passed
  • \cd backend && uv run ruff check\ + \uv run ruff format --check\ on the changed files ? clean

AI assistance

Tool(s) used: Doubao (AI coding assistant)

How you used it: The assistant implemented the defensive guards and the regression tests following the issue's reproduction steps and proposed approach, and ran the test and lint verification commands above. The diff is small and has been reviewed before submission.

  • I've read and understand every line of this change and take responsibility for it - it's not unreviewed AI output.

@CLAassistant

CLAassistant commented Sep 16, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@github-actions github-actions Bot added area:backend Gateway / runtime / core backend under backend/ risk:medium Medium risk: regular code changes size/S PR changes 20-100 lines labels Sep 16, 2026

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified at head 8874cfb: I reproduced the AttributeError on main's source for all five malformed payloads (NoneType/str/list/int object has no attribute 'get') and confirmed 19/19 tests pass in a detached worktree at this head (PYTHONPATH pinned to the worktree, so the head source was actually imported); ruff check is clean. The guard itself is correct, preserves the previously-working shapes, and the override-malformed -> default-file fallback is properly tested. One nit on type annotations inline. Follow-up note outside this diff: the PR description says the sibling Codex loader already guards the identical shape - that is true for the nested tokens value, but load_codex_cli_credential() still hits the same unguarded data.get("tokens", {}) AttributeError if ~/.codex/auth.json contains a top-level non-object JSON value, so the same defensive treatment may be worth a follow-up PR there.


def _extract_claude_code_credential(data: dict[str, Any], source: str) -> ClaudeCodeCredential | None:
oauth = data.get("claudeAiOauth", {})
if not isinstance(data, dict):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this runtime guard contradicts the module's static types. _extract_claude_code_credential still declares data: dict[str, Any], and its only caller _load_json_file declares -> dict[str, Any] | None even though json.loads can return any JSON value - so a type checker would flag this isinstance as unreachable (ruff doesn't, so CI stays green). Consider widening _load_json_file's return annotation to Any so the defensive checks read as reachable and the real contract is documented; alternatively move the top-level isinstance check into the load_claude_code_credential() loop and keep this helper strictly dict-typed. Behavior itself is correct - I verified these payloads raise AttributeError on main and return None at this head.

@WillemJiang

Copy link
Copy Markdown
Collaborator

@cyberspace-cs, please click the CLA assistant button to sign the CLA, then we can start the merge process.

@WillemJiang WillemJiang added this to the 2.1.0 milestone Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:backend Gateway / runtime / core backend under backend/ risk:medium Medium risk: regular code changes size/S PR changes 20-100 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Claude credential loader raises AttributeError when ~/.claude/.credentials.json has a non-object claudeAiOauth container

4 participants