Fix: Claude Code Auth Fails on Headless Linux

Written by Michael Lip · Solo founder of Zovo · $400K+ on Upwork · 100% JSS Join 50+ builders · More at zovo.one

The Error

After authenticating Claude Code on a headless Linux server (no display server, no browser), everything works for about an hour. Then:

HTTP 403 from Cloudflare WAF on token refresh:

POST https://platform.claude.com/v1/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=<token>&client_id=9d1c250a-e61b-44d9-88ed-5944d1962f5e

Response: 403 Forbidden (Cloudflare)
Subsequent retries: 429 Too Many Requests

Claude Code is permanently locked out until you manually re-authenticate, which itself requires the broken browser-based flow on a headless system.

Quick Fix

Use an API key to bypass OAuth entirely:

# On the headless server
export ANTHROPIC_API_KEY="sk-ant-api03-your-key-here"
claude

When ANTHROPIC_API_KEY is set, Claude Code uses it instead of your subscription, bypassing the browser-based OAuth flow completely. Get an API key from console.anthropic.com.

What Causes This

Claude Code’s OAuth tokens expire after approximately 1 hour (confirmed via the expiresAt field in ~/.claude/.credentials.json). When the CLI attempts to refresh the token, it sends a POST request to https://platform.claude.com/v1/oauth/token.

Cloudflare’s Web Application Firewall (WAF) classifies this request as automated bot traffic when it originates from a headless Linux server. The key difference from desktop environments: there is no browser context, no cookies, no JavaScript execution environment that would pass Cloudflare’s bot detection heuristics.

The failure chain:

  1. Access token expires (~1 hour after auth)
  2. CLI sends refresh request to OAuth endpoint
  3. Cloudflare WAF blocks the request (HTTP 403)
  4. CLI retries, hitting rate limits (HTTP 429)
  5. All subsequent operations fail
  6. Recovery requires full re-authentication, which needs a browser

Full Solution

Option 1: API Key Authentication (Most Reliable)

The ANTHROPIC_API_KEY environment variable bypasses OAuth entirely. When set, Claude Code uses API key authentication instead of your subscription, even if you are logged in.

# 1. Get an API key from console.anthropic.com
#    Navigate to: Settings > API Keys > Create Key

# 2. Set it on your headless server
echo 'export ANTHROPIC_API_KEY="sk-ant-api03-YOUR-KEY"' >> ~/.bashrc
source ~/.bashrc

# 3. Verify it works
claude -p "hello" --bare

For non-interactive use with -p, the API key is always used when present. For interactive sessions, Claude Code prompts you to confirm using the key once before it overrides your subscription.

Option 2: Pre-Generated OAuth Token

On a machine with a browser, authenticate with Claude Code and copy the token for use on the headless server:

# On the headless server:
export CLAUDE_CODE_OAUTH_TOKEN="your-token-here"

# Add to your shell profile for persistence:
echo 'export CLAUDE_CODE_OAUTH_TOKEN="your-token-here"' >> ~/.bashrc

Option 3: Credential File Transfer

# On a machine where auth works:
cat ~/.claude/.credentials.json

# Copy the entire JSON to the headless server:
mkdir -p ~/.claude
cat > ~/.claude/.credentials.json << 'CRED'
{paste the JSON content here}
CRED

# Note: This token will expire after ~1 hour and the same
# refresh problem will occur. You will need to repeat this
# process when the token expires.

Option 4: Automate Token Refresh from Desktop

If you frequently work on headless servers, set up a script on your desktop machine that refreshes the token and copies it:

#!/bin/bash
# refresh-claude-token.sh
# Run this on your desktop machine with a browser

# Extract the current token from credentials
TOKEN=$(cat ~/.claude/.credentials.json | python3 -c "
import json, sys
creds = json.load(sys.stdin)
print(creds.get('oauthToken', ''))
")

# Copy to headless server
ssh user@headless-server "
  mkdir -p ~/.claude
  echo 'export CLAUDE_CODE_OAUTH_TOKEN=\"$TOKEN\"' > ~/.claude/env
"

Prevention