Fix: Claude Code Auth Fails on Headless Linux
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:
- Access token expires (~1 hour after auth)
- CLI sends refresh request to OAuth endpoint
- Cloudflare WAF blocks the request (HTTP 403)
- CLI retries, hitting rate limits (HTTP 429)
- All subsequent operations fail
- 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
- Default to API keys on headless servers – add
ANTHROPIC_API_KEYto your server provisioning scripts or dotfiles - For CI/CD pipelines, store the API key as a GitHub Actions secret or equivalent and inject via environment variable
- Never rely on interactive OAuth for unattended or headless environments
- Monitor token expiry: check
~/.claude/.credentials.jsonforexpiresAttimestamps if you must use OAuth