|
| 1 | +# The Anthropic OAuth posture, and the exit |
| 2 | + |
| 3 | +Status: standing. Revisit when a refresh starts failing or a policy notice |
| 4 | +arrives. |
| 5 | + |
| 6 | +## How figaro talks to Anthropic today |
| 7 | + |
| 8 | +The anthropic providers present the **Claude Code OAuth credential** (a |
| 9 | +claude.ai subscription) and identify as Claude Code itself: |
| 10 | +`User-Agent: claude-cli/<version>`, the `claude-code-20250219` beta header, |
| 11 | +and a claimed client version (`anthropicmodels.ClaudeCodeVersion`). Token |
| 12 | +mint and refresh live in **hush**, not here. |
| 13 | + |
| 14 | +This works precisely because the requests are indistinguishable from the |
| 15 | +real client. It is subscription-priced inference for an agent harness, and |
| 16 | +it is the arrangement this document is honest about. |
| 17 | + |
| 18 | +## What the ground looks like (September 2026) |
| 19 | + |
| 20 | +Three facts, found the day Fable 5.1 started refusing us: |
| 21 | + |
| 22 | +1. **Models are gated on the claimed client version.** Fable 5.1 returns |
| 23 | + 400 `claude_code_version_too_old` below 2.1.251. The check is a numeric |
| 24 | + threshold, not a whitelist of real releases — and the floor sits *above* |
| 25 | + the stable channel, so even genuine Claude Code on stable fails |
| 26 | + (anthropics/claude-code#91345). Hence the rule on the constant: track |
| 27 | + `dist-tags.latest`. |
| 28 | + |
| 29 | +2. **The policy line is now explicit.** The Claude Code docs were updated |
| 30 | + to ban OAuth token usage in third-party tools; `claude setup-token` |
| 31 | + credentials (`sk-ant-oat01-*`) are rejected by the Messages API |
| 32 | + (claude-code#28091, #37205). What figaro does is not a gray area in the |
| 33 | + text; it is a gray area only in enforcement. |
| 34 | + |
| 35 | +3. **Enforcement is visibly tightening.** Since ~June 2026 the token |
| 36 | + endpoint (`/v1/oauth/token`) refuses `claude-cli/*` User-Agents outright |
| 37 | + (hermes-agent#48534) — an anti-abuse net aimed at exactly this pattern. |
| 38 | + That is the *refresh* path, which is hush's; figaro's turns work today |
| 39 | + because hush's refresh passes today. |
| 40 | + |
| 41 | +## The decision |
| 42 | + |
| 43 | +**If OAuth becomes non-viable, figaro changes providers. That is the whole |
| 44 | +contingency.** No countermeasures, no UA arms race: the moment the |
| 45 | +arrangement stops working — refreshes blocked, version gates that cannot be |
| 46 | +cleared, account action — we switch credentials, not disguises. |
| 47 | + |
| 48 | +The exit is already built and deliberately boring: |
| 49 | + |
| 50 | +- **A plain API key**, via `api_key_file` (`auth.FileValue`): first in the |
| 51 | + resolution chain, no impersonation anywhere on the path, and the shape a |
| 52 | + server deployment uses anyway (sops → `/run/secrets` → path). Metered |
| 53 | + pricing instead of subscription; that is the cost of the exit, and it is |
| 54 | + a price question rather than an architecture question. |
| 55 | +- **Copilot**, already a first-class provider, carrying most of the same |
| 56 | + models under its own sanctioned agent quota. |
| 57 | +- **Any OpenAI-compatible endpoint** through the openaichat provider. |
| 58 | + |
| 59 | +## What to watch, and what each sign means |
| 60 | + |
| 61 | +| sign | meaning | response | |
| 62 | +|---|---|---| |
| 63 | +| 400 `claude_code_version_too_old` | routine version-floor bump | bump `ClaudeCodeVersion` to npm `latest` | |
| 64 | +| refreshes failing / 429 from `/v1/oauth/token` | the UA net caught the refresh path | check hush's UA on that endpoint; if intentional targeting, take the exit | |
| 65 | +| 401s with a valid, fresh token | credential class revoked | take the exit | |
| 66 | +| policy notice on the account | the question has been asked directly | take the exit | |
| 67 | + |
| 68 | +The first row is maintenance. The other three are the same instruction. |
0 commit comments