fix(claude-usage): query the chat org, not memberships[0] - #219
Conversation
The usage fetch worked through bootstrap (org resolves) but the org-scoped /usage endpoint returned 403 "Invalid authorization for organization", surfaced to the user as an expired/invalid key — so a valid session key was stuck in a permanent reconfigure loop. Root cause (confirmed against live claude.ai): claude.ai accounts can belong to multiple organizations, and the /usage endpoint is only authorized for the org with claude.ai access — the one carrying the `chat`/`claude_max` capability. The scanner blindly used `memberships[0]`, which for any user who has also used the Anthropic API is the separate `api`-capability org (often listed first) and 403s. ADR-035 dropped the manual org field and switched to bootstrap resolution but left the selection heuristic unspecified; `[0]` was the unstated — and wrong — default for multi-org accounts. Fix: `selectUsageOrg(memberships)` selects the org whose capabilities include `chat` (or `claude_max`), falling back to the sole org only when it carries no capability signal at all (preserving single-org back-compat). A lone org that explicitly lacks chat access returns null rather than a uuid guaranteed to 403. The `no_org` message now names both causes — an expired key OR an account with no Pro/Max subscription — so a valid-key API-only user isn't wrongly told their key expired. The bootstrap diagnostic log now includes per-org capability names (never the cookie) to make org-selection issues debuggable. Verified end-to-end: the real scanner against live claude.ai with a real key returns actual usage data (was 403 before). 24 scanner tests (8 new for org selection + message), full suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BNCGtrKPo66XyqvNFBinzX
nox-0x
left a comment
There was a problem hiding this comment.
Approving — root cause is well-diagnosed, selectUsageOrg is small and defensively coded (chat → claude_max → sole-org-no-caps fallback → null), and the new tests cover the meaningful branches (multi-org chat-preferred, claude_max fallback, lone-org-without-caps, lone-org-with-non-chat-caps → null, multi-org-no-chat → null, empty → null, and end-to-end that the api org is never queried).
The fix is also a strict improvement on the previous behavior: even in the worst-case shape (multiple orgs, none advertising chat), returning null surfaces an actionable "no_org" message instead of guaranteed-to-403 traffic, and the diagnostic log records capabilities so any future drift (e.g. a Teams/Enterprise plan using a different capability name) is visible from server logs without leaking the cookie.
The reworded no_org message correctly names both causes so an API-only user with a valid key isn't sent into a reconfigure loop. Nothing to block on.
Four append-only ADRs documenting decisions shipped across PRs #220, #219, #221, and #222: - ADR-039 — Terminal-only view, xterm.js-only renderer (#220, Cleanup@autonomOS) - ADR-040 — selectUsageOrg() picks chat/claude_max capability (#219, ClaudeUsage) - ADR-041 — Zero-touch Claude Usage via in-memory cookie harvest (#221, ClaudeUsage) - ADR-042 — Insecure-context clipboard fallback for OSC 52 (#222, RemoteCopy) Bundled to keep the docs cadence dense — each ADR is owned by the agent who shipped the underlying code. ADR-038 and prior entries untouched. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Problem
PR #218 fixed the Claude Usage plugin's error messaging, but the underlying fetch still never returned data — it just failed more honestly. Terry asked the load-bearing question: "why doesn't it actually work?" This PR is the answer.
The fetch reached the org-scoped usage endpoint and got HTTP 403
permission_error: "Invalid authorization for organization", which the scanner mapped tounauthorized→ "Session key expired or invalid" → a permanent reconfigure loop for a valid key.Root cause (confirmed against live claude.ai)
claude.ai accounts can belong to multiple organizations, and the
/usageendpoint is only authorized for the org with claude.ai access — the one carrying thechat/claude_maxcapability. The scanner blindly usedmemberships[0].flowchart LR K[Valid sessionKey] --> B[GET /api/bootstrap] B -->|200, resolves org| S{which membership?} S -->|"memberships[0]<br/>caps: api, api_individual"| A["GET /org/{api}/usage"] A -->|"403 Invalid authorization"| X["mapped to 'expired key'<br/>→ reconfigure loop"] S -->|"the chat org<br/>caps: chat, claude_max"| C["GET /org/{chat}/usage"] C -->|"200 ✅ real usage data"| D["5h 17% · 7d 0%"] style X fill:#ea6c7322 style D fill:#23863622For anyone who has also used the Anthropic API (most autonomOS users),
memberships[0]is the separate API/console org → 403. The real Claude Max subscription lives in a different membership.This is the unstated heuristic gap from ADR-035: it dropped the manual org field and switched to bootstrap resolution but never specified which membership to pick;
[0]was the wrong default. (TeamLead is landing ADR-040 to record theselectUsageOrgheuristic — this PR refines, not reverses, ADR-035.)Fix
selectUsageOrg(memberships):chat(orclaude_max).nullrather than a uuid guaranteed to 403.no_orgmessage now names both causes — expired key or no Pro/Max subscription — so a valid-key API-only user isn't wrongly told their key expired.Testing
no_orgmessage). 24 scanner tests total, all green.fiveHour: 17%) where it previously 403'd. This is stronger than browser QA — it's the realimpitfetch hitting the real endpoint.no_orgmessage, doomed sole-org fallback) addressed in this PR.Risks
/usageis gated on thechatcapability — verified true for the test account; the diagnostic log surfaces any future drift.🤖 Generated with Claude Code
https://claude.ai/code/session_01BNCGtrKPo66XyqvNFBinzX