Skip to content

fix(claude-usage): query the chat org, not memberships[0] - #219

Merged
aterrylu merged 1 commit into
mainfrom
terry/claude-usage-wrong-org
Jun 20, 2026
Merged

fix(claude-usage): query the chat org, not memberships[0]#219
aterrylu merged 1 commit into
mainfrom
terry/claude-usage-wrong-org

Conversation

@aterrylu

Copy link
Copy Markdown
Owner

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 to unauthorized → "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 /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].

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:#23863622
Loading

For 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 the selectUsageOrg heuristic — this PR refines, not reverses, ADR-035.)

Fix

selectUsageOrg(memberships):

  • Picks the org whose capabilities include chat (or claude_max).
  • Falls back to the sole org only when it carries no capability signal (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 — expired key or 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) for debuggability.

Testing

  • 8 new scanner tests (chat-over-api ordering, claude_max fallback, sole-org no-caps fallback, lone-org-explicit-non-chat → null, multi-org-no-chat → null, empty → null, end-to-end "queries chat not api", and the dual-cause no_org message). 24 scanner tests total, all green.
  • End-to-end against live claude.ai with a real key: the actual scanner now returns real usage data (fiveHour: 17%) where it previously 403'd. This is stronger than browser QA — it's the real impit fetch hitting the real endpoint.
  • Full suites green: 467 server + 216 dashboard, tsc + biome clean.
  • Polish: code-reviewer + silent-failure-hunter — both findings (misleading no_org message, doomed sole-org fallback) addressed in this PR.

Risks

  • The heuristic assumes /usage is gated on the chat capability — verified true for the test account; the diagnostic log surfaces any future drift.
  • No regression for previously-working single-chat-org users (chat match returns before any fallback).

🤖 Generated with Claude Code

https://claude.ai/code/session_01BNCGtrKPo66XyqvNFBinzX

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
@aterrylu
aterrylu enabled auto-merge (squash) June 20, 2026 00:11

@nox-0x nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

@aterrylu
aterrylu merged commit e8e8c77 into main Jun 20, 2026
10 checks passed
@aterrylu
aterrylu deleted the terry/claude-usage-wrong-org branch June 20, 2026 00:13
aterrylu added a commit that referenced this pull request Jun 20, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants