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\b → Claude Code (now longer) |
11,157 |
200 OK |
subscription bucket |
harness prompt, only harness → CLI |
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\b → Claude 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_start → systemPrompt 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):
- 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".)
- 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.
Summary
Running Shannon on a Claude Pro/Max/Team subscription via
CLAUDE_CODE_OAUTH_TOKENworks 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:…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
main(post-#389 pi migration)@earendil-works/pi-*0.82.1CLAUDE_CODE_OAUTH_TOKEN(sk-ant-oat01-…, fromclaude setup-token), Claude Teamanthropic,api.anthropic.comclaude-sonnet-4-6What actually happens on the wire
With a
sk-ant-oattoken, pi already does the right thing:isOAuthToken()inpi-ai/dist/api/anthropic-messages.js:636switches the request toAuthorization: Bearer, dropsx-api-key, addsanthropic-beta: claude-code-20250219,oauth-2025-04-20,user-agent: claude-cli/<v>,x-app: cli, and prepends theYou are Claude Code, …identity block. So OAuth is not removed in V2 —CLAUDE_CODE_OAUTH_TOKENis 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: 16each):ratelimit-unified-reset\bpi\b→Claude Code(now longer)harness→CLIThree rounds alternating raw vs. name-sanitized: raw 0/3 accepted, sanitized 3/3. The two variants land in different
ratelimit-unified-resetbuckets — the visible proof of two billing lanes.Conclusions:
pi. Removing it everywhere flips the lane; removing onlyharnessor only the opening sentence does not.Proposed fix
A small pi extension that rewrites
\bpi\b→Claude Codein the harness system prompt for OAuth runs only (self-gated on thesk-ant-oatmarker, a no-op otherwise). It uses the existingbefore_agent_start→systemPrompthook, loaded the same way as the currentbash-timeoutextension. 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):
ANTHROPIC_API_KEYandCLAUDE_CODE_OAUTH_TOKENshare theanthropicprovider, 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 staleANTHROPIC_API_KEYexported quietly bills API credits. (V1 hard-errored here — "Multiple providers detected".)CLAUDE_CODE_OAUTH_TOKENwithout thesk-ant-oatmarker is sent as anx-api-keywith no warning.Note on the current docs
README.md:97currently points subscription users to theshannon-v1branch, 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.