$ cat ./blog/claude-oauth-detective-story.md

The Case of the Forbidden System Prompt

--ai --oauth --authentication --debugging --anthropic

How a single sentence—precisely 57 characters—stood between a working OAuth flow and the infuriating message 'This credential is only authorized for use with Claude Code.'

It started, as these things often do, with everything appearing to work perfectly.

The OAuth flow completed without complaint. Tokens arrived bearing the proper sk-ant-oat prefix. The profile endpoint returned my account details with the crisp efficiency of a well-run bureaucracy. Refresh tokens refreshed. The authentication dance, in other words, was flawless.

And yet.

Every attempt to actually use these lovingly-obtained credentials for inference—the entire point of the exercise—met the same terse rejection: “This credential is only authorized for use with Claude Code.”

If you’ve spent any time in software development, you’ll recognize this particular flavor of frustration. The kind where the system is telling you something, but the something it’s telling you is, by any reasonable interpretation, demonstrably false. The credentials were from Claude Code. The OAuth flow was identical to the official CLI’s. The headers were present and accounted for.

And still: This credential is only authorized for use with Claude Code.

The Usual Suspects

The investigation proceeded along predictable lines. Headers, naturally, were the first place to look. Anthropic’s OAuth 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",
]

Present. Verified. Quadruple-checked.

The x-api-key header, which must not be present when using OAuth? Deleted with prejudice. The Authorization: Bearer header? Correctly formatted. The User-Agent string mimicking Claude Code’s own signature? In place.

Model restrictions were investigated. Perhaps OAuth tokens could only access certain models? But no—the error appeared regardless of whether one requested the humble Haiku or the mighty Opus.

Network interception revealed the requests to be byte-for-byte identical to what the official Claude Code CLI transmitted. And yet one worked and one didn’t.

The problem, it would eventually emerge, was hiding in the most mundane location imaginable.

The Body of Evidence

Authentication systems typically concern themselves with headers. Tokens live in the Authorization header. API keys in their x-api-key homes. This is the natural order of things.

But Anthropic, in what one might charitably describe as an architecturally unusual decision, had placed an additional gate in an unexpected location: the request body itself.

Specifically, the system field.

For Claude Code OAuth tokens, the API performs what amounts to a string comparison on the system prompt. Not a prefix check. Not a contains check. An exact match:

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

That’s it. Fifty-seven characters. Not fifty-eight.

Adding anything after that sentence—additional instructions, newlines, even a humble \nfoo—triggers the authentication failure. The OAuth flow worked. The headers were correct. But the moment the request body’s system prompt contained so much as an extra character, the API rejected the entire affair with the same maddening message about credentials.

The Fix

The solution, once understood, was almost comically simple:

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 is now exactly the blessed phrase. Any custom instructions must find alternative accommodations—perhaps in the user message, perhaps via an API key instead.

The Lesson

There is, of course, a reasonable security rationale here. OAuth tokens obtained through Claude Code’s flow are tied to consumer subscriptions (Claude Pro, Claude Max). By restricting what system prompts these tokens can use, Anthropic ensures they’re being used for their intended purpose—running Claude Code—rather than powering arbitrary third-party applications on consumer billing.

But the implementation creates a rather elegant trap. Every piece of the authentication puzzle can be correct—client ID, PKCE challenge, token exchange, headers, beta flags—and the system will still reject you if you’ve committed the sin of having a custom system prompt.

The documentation, such as it exists, mentions that OAuth tokens require “the Claude Code header.” What it doesn’t quite convey is that the header must appear alone, orphaned from any additional context that might make your agent actually useful.

For those building Claude Code-compatible tools, the takeaway is clear: if you need custom system instructions, you need an API key. If you’re using OAuth, your system prompt is not your own.

The credentials were, in fact, only authorized for use with Claude Code.

Just not in the way I initially understood that sentence.


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.