On January 3, the OAuth flow did exactly what it was supposed to do.

PKCE completed. Tokens arrived with the expected sk-ant-oat prefix. The profile endpoint returned my account details. Refresh tokens refreshed. From the authentication side of the house, the credentials looked real.

Inference failed on the first real request, with a sentence that seemed to contradict every check above it:

“This credential is only authorized for use with Claude Code.”

Confusion came from the mismatch. Credentials came from the Claude Code OAuth flow. The request path matched the official CLI. The error message sounded like an authorization boundary, but the obvious authorization evidence was already in place.

At that point the job was not to make the request more plausible. It was to find the one thing the server was actually checking.

Plausible suspects

Headers were the first suspect. Anthropic’s OAuth path requires a particular set of beta flags:

const OAUTH_BETAS = [
  "oauth-2025-04-20",
  "interleaved-thinking-2025-05-14",
  "fine-grained-tool-streaming-2025-05-14",
  "context-management-2025-06-27",
]

All four were present.

No x-api-key header was present, which is correct for OAuth. The Authorization: Bearer header was correctly formatted. The User-Agent string matched Claude Code’s own signature.

Model restrictions came next. The failure appeared regardless of whether the request targeted Haiku or Opus, so the model was not the gate.

Network comparison made the problem narrower. The request looked like what the official Claude Code CLI transmitted in the places I was checking. One worked. Mine did not.

Whatever was missing was not in the usual authentication furniture.

A painterly editorial collage for Claude OAuth newline bug, showing the concrete objects and system relationships around credential flow, system prompt, and evidence cards.
Credential flow, system prompt, and evidence cards.

Gate was in the body

Authentication failures train you to stare at headers. Tokens live in Authorization. API keys live in x-api-key. Beta flags live in their own header. That is where the first hour usually goes.

The gate was in the request body.

Specifically, it was in the system field.

For Claude Code OAuth tokens, the API expected the system prompt to match one sentence exactly:

You are Claude Code, Anthropic's official CLI for Claude.

Fifty-seven characters. Not fifty-eight.

Adding anything after that sentence, including additional instructions or a newline, triggered the same credential error. The OAuth flow worked. The headers were correct. The token was valid. The request body made it invalid for this purpose.

Fifty-seven characters, no more

Once the check was visible, the fix was small:

function getOauthSystemPrompt(systemPrompt: string): string {
  if (process.env.CODING_AGENT_OAUTH_SYSTEM_MODE === "full") {
    // Experimental: use full system prompt (likely to fail auth)
    return `${CLAUDE_CODE_SYSTEM_HEADER}\n\n${systemPrompt}`
  }
  // OAuth tokens require exactly this string. No more. No less.
  return CLAUDE_CODE_SYSTEM_HEADER
}

In OAuth mode, the system prompt now stays exactly on the allowed sentence. Custom instructions have to move somewhere else, or the tool needs to use an API key instead of a Claude Code OAuth token.

OAuth meant Claude Code only

A reasonable security shape sits underneath this. OAuth tokens obtained through Claude Code’s flow are tied to consumer subscriptions. Restricting the system prompt keeps those tokens aligned with their intended use: running Claude Code, not powering arbitrary third-party applications on consumer billing.

Every obvious piece of the authentication puzzle can be correct: client ID, PKCE challenge, token exchange, headers, beta flags, refresh path. A custom system prompt in the body is still enough to produce the failure.

Documentation mentions that OAuth tokens require the Claude Code header. The practical version is stricter: the header must appear alone.

For Claude Code-compatible tools, this changes the boundary. If the tool needs its own system instructions, it needs an API key. If it uses OAuth, the system prompt is not the place to put product behavior.

The error message had been true.

I had been reading it at the wrong layer.


The code that emerged from this investigation lives in an open-source coding agent at github.com/chrischabot/coding-agent-poc. The authentication documentation, considerably expanded after this adventure, can be found in claude-auth.md.

Chris Chabot · January 2026