Skip to content

Anthropic OAuth (Claude subscription) is billed to metered "extra usage" — pi's system prompt name is the fingerprint #408

Description

@mguttmann

Summary

Running Shannon on a Claude Pro/Max/Team subscription via CLAUDE_CODE_OAUTH_TOKEN works at the transport level — pi already sends the correct OAuth request — but every request is billed against the pay-per-token "extra usage" pool instead of the subscription's included quota. Once that pool is empty (most subscribers never fund it), every request fails:

HTTP 429  This request would exceed your account's monthly spend limit. Please try again later.

…while the claude.ai usage panel shows the subscription barely touched. Native Claude Code on the same account never hits this.

The cause is not the token, the account, or the harness — it is one word in the system prompt. pi's harness system prompt names the product "pi", and Anthropic's OAuth billing classifier fingerprints that as third-party API usage and routes the request to the metered lane.

This mirrors, in another harness, the root cause bisected in NousResearch/hermes-agent#72171.

Environment

Shannon main (post-#389 pi migration)
Harness @earendil-works/pi-* 0.82.1
Auth CLAUDE_CODE_OAUTH_TOKEN (sk-ant-oat01-…, from claude setup-token), Claude Team
Provider anthropic, api.anthropic.com
Model claude-sonnet-4-6

What actually happens on the wire

With a sk-ant-oat token, pi already does the right thing: isOAuthToken() in pi-ai/dist/api/anthropic-messages.js:636 switches the request to Authorization: Bearer, drops x-api-key, adds anthropic-beta: claude-code-20250219,oauth-2025-04-20, user-agent: claude-cli/<v>, x-app: cli, and prepends the You are Claude Code, … identity block. So OAuth is not removed in V2 — CLAUDE_CODE_OAUTH_TOKEN is still forwarded (apps/worker/src/ai/models.ts:39), documented (docs/ai-providers.md:15), and accepted by the setup wizard.

The problem is the second system block: pi's own harness prompt (~11k chars) that names the product "pi" ~13 times. That is what the classifier reads.

Reproduction / bisection

Same credential, same tiny user prompt, only the second system[] block varies. Alternated in a single run so a limit reset cannot explain it (max_tokens: 16 each):

system[] second block Length Result ratelimit-unified-reset
pi harness prompt (as shipped) 11,027 429 monthly spend limit metered bucket
generic filler, same length 11,027 200 OK subscription bucket
harness prompt trimmed to 2,958 2,958 429 metered bucket
harness prompt, only \bpi\bClaude Code (now longer) 11,157 200 OK subscription bucket
harness prompt, only harnessCLI 11,027 429 metered bucket
harness prompt, only first sentence rewritten ~11,027 429 metered bucket

Three rounds alternating raw vs. name-sanitized: raw 0/3 accepted, sanitized 3/3. The two variants land in different ratelimit-unified-reset buckets — the visible proof of two billing lanes.

Conclusions:

  • Not a length limit. Same length, opposite outcome; the accepted variant is longer than the rejected one.
  • The fingerprint is the product name pi. Removing it everywhere flips the lane; removing only harness or only the opening sentence does not.
  • Trimming does not help — the name is in the prompt's head.

Proposed fix

A small pi extension that rewrites \bpi\bClaude Code in the harness system prompt for OAuth runs only (self-gated on the sk-ant-oat marker, a no-op otherwise). It uses the existing before_agent_startsystemPrompt hook, loaded the same way as the current bash-timeout extension. Shannon's own agent prompts are untouched — they travel as user messages, which the classifier does not appear to inspect (verified: a real multi-turn recon scan with tool use and a custom collector tool ran entirely on the subscription lane, 0/4 requests rejected).

A PR follows. It's ~55 lines plus wiring, no new dependency, no transport change.

Two related, smaller observations

While tracing this I hit two silent failure modes worth fixing (the PR de-silences both as warnings):

  1. Silent API-key precedence. ANTHROPIC_API_KEY and CLAUDE_CODE_OAUTH_TOKEN share the anthropic provider, so the "exactly one provider" gate (apps/cli/src/env.ts) never fires when both are set; resolveProviderCredentials (apps/worker/src/ai/models.ts:137-150) then silently picks the API key. A user who sets the OAuth token but has a stale ANTHROPIC_API_KEY exported quietly bills API credits. (V1 hard-errored here — "Multiple providers detected".)
  2. No token-shape check. A CLAUDE_CODE_OAUTH_TOKEN without the sk-ant-oat marker is sent as an x-api-key with no warning.

Note on the current docs

README.md:97 currently points subscription users to the shannon-v1 branch, and the maintainer note in #398 states the subscription "can't be used" on pi. Based on the bisection above that describes the symptom of this bug, not a hard limitation: with a Claude-Code-shaped payload the identical account bills against the included quota. If this diagnosis is accepted, those should be revisited.

Honest caveat — ToS

This works by making pi's payload indistinguishable from Claude Code so the subscription lane accepts it. Anthropic deliberately distinguishes first-party Claude Code from third-party harnesses, and their terms reserve enforcement without notice. This is the same tradeoff spelled out in hermes-agent#72171; flagging it explicitly so the decision to adopt (or not) is informed rather than implicit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions