Skip to content

Instantly share code, notes, and snippets.

@whistlermike
Last active March 11, 2026 03:24
Show Gist options
  • Select an option

  • Save whistlermike/6e4a81ce5749de8c32df69b1a77076eb to your computer and use it in GitHub Desktop.

Select an option

Save whistlermike/6e4a81ce5749de8c32df69b1a77076eb to your computer and use it in GitHub Desktop.
OpenClaw Anthropic Auth Guide — adding accounts, rotation order, fix-dude maintenance

OpenClaw Anthropic Auth Guide

Covers: adding accounts, refreshing tokens, rotation order, and maintaining fix-dude.


Key Files

File Purpose
~/.openclaw/agents/main/agent/auth-profiles.json The real auth store — profiles, tokens, rotation order, cooldown state
~/.claude/.credentials.json Claude Code OAuth credentials — contains refresh token used by fix-dude's OAuth refresh attempt
~/.zshrc Contains the fix-dude shell function
/tmp/pkce_start.py PKCE OAuth flow helper script (lives in /tmp — recreate after reboot)

Note: ~/.openclaw/openclaw.json has an auth section but the main agent does not use it. It reads exclusively from auth-profiles.json.


Auth Profile Types

All profiles use type: "token" regardless of whether the underlying token is an OAuth token or API key.

"anthropic:my-account": {
  "type": "token",
  "provider": "anthropic",
  "token": "sk-ant-oat01-..."
}

⚠️ Do NOT use type: "oauth" — using Bearer auth causes HTTP 403 "OAuth authentication is currently not allowed for this organization" from Anthropic's API. All sk-ant-oat01-* tokens must be sent as x-api-key via type: "token".


Current Rotation (as of 2026-03-09)

# Profile ID Notes
1 anthropic:mike@OurKith Primary account — fix-dude attempts OAuth refresh, preserves existing token on failure
2 anthropic:mike@anthemx.ca
3 anthropic:yeahdog@anthemX Signed in as yeahdog@gmail.com
4 anthropic:mikeDos
5 anthropic:mike@addyinvest.com
6 anthropic:thedudeabidesai@gmail.com

mike-personal: removed when near weekly cap; restore by adding back to fix-dude order.


Quick Token Refresh (via Claude Code)

This is the easiest way to refresh expired tokens. No manual curl or Python exchange needed.

Prerequisites

Create the PKCE helper script (recreate after reboot):

cat > /tmp/pkce_start.py << 'PYEOF'
import base64, hashlib, os, json, urllib.parse

CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
REDIRECT = "https://console.anthropic.com/oauth/code/callback"
AUTH_URL = "https://claude.ai/oauth/authorize"

verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b"=").decode()
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()

params = urllib.parse.urlencode({
    "code": "true",
    "client_id": CLIENT_ID,
    "response_type": "code",
    "redirect_uri": REDIRECT,
    "scope": "org:create_api_key user:profile user:inference",
    "code_challenge": challenge,
    "code_challenge_method": "S256",
    "state": verifier,
})

print(f"{AUTH_URL}?{params}")
json.dump({"verifier": verifier}, open("/tmp/pkce.json", "w"))
PYEOF

Per-account refresh

For each account that needs refreshing:

  1. In a separate terminal, run python3 /tmp/pkce_start.py
  2. Open the printed URL in your browser
  3. Sign in as the target account
  4. Copy the code#state string from the callback
  5. Paste into Claude Code like this:
mike@anthemx.ca ABC123def456#XYZ789abc

Claude Code will automatically:

  • Split the code and state
  • Run the curl token exchange
  • Update auth-profiles.json with the new token
  • Confirm success

⚠️ Auth codes expire in ~60 seconds. Paste into Claude Code promptly after getting the code.

Bulk refresh (all tokens expired)

When all tokens are dead (e.g. after a wipe), repeat the per-account flow for each profile. You must run python3 /tmp/pkce_start.py fresh before each account (each run generates a new PKCE verifier).

After all accounts are refreshed, restart the gateway:

launchctl bootout gui/$UID/ai.openclaw.gateway 2>/dev/null; true
kill -9 $(lsof -ti :18789) 2>/dev/null; true
sleep 3
openclaw gateway install --force

Or just run source ~/.zshrc && fix-dude which also clears cooldowns.


Adding a New Account

Step 1 — Trigger the OAuth flow

python3 /tmp/pkce_start.py

Open the printed URL, sign in as the new account, copy the code#state string.

Step 2 — Exchange and save (via Claude Code)

Paste into Claude Code:

newaccount@example.com CODE#STATE

Claude handles the exchange and adds the profile to auth-profiles.json.

Step 3 — Add to rotation order

If it's a brand new account (not just a refresh), also add it to the rotation order in auth-profiles.json and in fix-dude's order list in ~/.zshrc.

Step 4 — Verify the token works

For API plan accounts, test directly:

curl -s -X POST https://api.anthropic.com/v1/messages \
  -H "x-api-key: sk-ant-oat01-..." \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{"model":"claude-haiku-4-5-20251001","max_tokens":10,"messages":[{"role":"user","content":"hi"}]}'

For Max plan accounts (no direct API access), invalid x-api-key is expected from curl. Verify through gateway:

  1. Move new profile to front of rotation order
  2. Restart gateway: source ~/.zshrc && fix-dude
  3. Send a message through Discord
  4. Check usageStats shows lastUsed with errorCount: 0

Step 5 — Update fix-dude and restart

Add new profile to order in ~/.zshrc, then: source ~/.zshrc && fix-dude


Removing an Account

Temporary removal (e.g. weekly cap hit)

import json

auth_path = '/Users/agentclaw/.openclaw/agents/main/agent/auth-profiles.json'
with open(auth_path) as f: d = json.load(f)
d['order']['anthropic'].remove('anthropic:mike-personal')
with open(auth_path, 'w') as f: json.dump(d, f, indent=2)

Then restart: source ~/.zshrc && fix-dude

Permanent removal

import json

auth_path = '/Users/agentclaw/.openclaw/agents/main/agent/auth-profiles.json'
with open(auth_path) as f: d = json.load(f)
del d['profiles']['anthropic:account-to-remove']
d['order']['anthropic'].remove('anthropic:account-to-remove')
d.get('usageStats', {}).pop('anthropic:account-to-remove', None)
with open(auth_path, 'w') as f: json.dump(d, f, indent=2)

Then remove from fix-dude order and run source ~/.zshrc && fix-dude.


Changing the Rotation Order

import json

auth_path = '/Users/agentclaw/.openclaw/agents/main/agent/auth-profiles.json'
with open(auth_path) as f: d = json.load(f)
d['order']['anthropic'] = [
    'anthropic:mike@OurKith',
    'anthropic:mike@anthemx.ca',
    'anthropic:yeahdog@anthemX',
    'anthropic:mikeDos',
    'anthropic:mike@addyinvest.com',
    'anthropic:thedudeabidesai@gmail.com',
]
with open(auth_path, 'w') as f: json.dump(d, f, indent=2)

Then restart the gateway. Also update fix-dude to match.


fix-dude Reference

What it does

  1. Tries to refresh OurKith OAuth token via https://console.anthropic.com/v1/oauth/token
  2. If refresh succeeds, writes new token to auth-profiles.json and syncs to credentials.json
  3. If refresh fails, preserves the existing token in auth-profiles.json (no credentials.json fallback — removed because it overwrote fresh PKCE tokens with stale ones)
  4. Resets rotation order to canonical list
  5. Clears all cooldowns, failureCounts, and errorCounts
  6. Sets lastGood to anthropic:mike@OurKith
  7. Stops gateway via launchctl bootout + kill -9, restarts with openclaw gateway install --force

When to run it

  • After hitting "All models failed (rate_limit)"
  • After adding or removing a profile
  • After changing the order
  • When OurKith stops working
  • Any time the gateway is misbehaving

How to run it

source ~/.zshrc && fix-dude

Cannot be run from inside Claude Code. Use a separate terminal.

Current fix-dude source (~/.zshrc)

fix-dude() {
  python3 -c "
import json, time, urllib.request

CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e'
creds_path = '/Users/agentclaw/.claude/.credentials.json'
auth_path  = '/Users/agentclaw/.openclaw/agents/main/agent/auth-profiles.json'

with open(auth_path) as f: d = json.load(f)

ourkith_token = None

# Try to refresh OurKith token via OAuth endpoint
print('Refreshing OurKith token...')
try:
    with open(creds_path) as f: creds = json.load(f)
    rt = creds['claudeAiOauth']['refreshToken']
    payload = json.dumps({'grant_type':'refresh_token','client_id':CLIENT_ID,'refresh_token':rt}).encode()
    req = urllib.request.Request('https://console.anthropic.com/v1/oauth/token', data=payload, headers={'Content-Type':'application/json'}, method='POST')
    with urllib.request.urlopen(req) as resp: r = json.loads(resp.read())
    new_expires = int(time.time() * 1000) + r['expires_in'] * 1000 - 5 * 60 * 1000
    ourkith_token = r['access_token']
    creds['claudeAiOauth'].update({'accessToken': ourkith_token, 'refreshToken': r.get('refresh_token', rt), 'expiresAt': new_expires})
    with open(creds_path, 'w') as f: json.dump(creds, f, indent=2)
    import datetime
    print(f'OurKith token refreshed. Expires: {datetime.datetime.fromtimestamp(new_expires/1000).strftime(\"%H:%M\")}')
except Exception as e:
    print(f'Refresh failed: {e} — keeping existing OurKith token in auth-profiles.json')
    # Do NOT fall back to credentials.json — auth-profiles.json may have a fresher PKCE token

if ourkith_token:
    d['profiles']['anthropic:mike@OurKith'] = {'type':'token','provider':'anthropic','token':ourkith_token}
else:
    print('OurKith token unchanged (existing token preserved)')

d['order']['anthropic'] = ['anthropic:mike@OurKith','anthropic:mike@anthemx.ca','anthropic:yeahdog@anthemX','anthropic:mikeDos','anthropic:mike@addyinvest.com','anthropic:thedudeabidesai@gmail.com']
for p in d.get('usageStats',{}):
    if 'anthropic' in p:
        d['usageStats'][p].pop('cooldownUntil',None)
        d['usageStats'][p].pop('failureCounts',None)
        d['usageStats'][p]['errorCount'] = 0
d['lastGood']['anthropic'] = 'anthropic:mike@OurKith'
with open(auth_path,'w') as f: json.dump(d,f,indent=2)
print('Auth profiles reset.')
"
  launchctl bootout gui/$UID/ai.openclaw.gateway 2>/dev/null; true
  lsof -ti :18789 | xargs kill -9 2>/dev/null; true
  sleep 3
  openclaw gateway install --force
  echo "The Dude abides."
}

OurKith Refresh Failure

Output Severity Action
Refresh failed: ... — keeping existing OurKith token then Auth profiles reset. None Existing PKCE token preserved — gateway works fine
OurKith 401s during actual model calls Action required Re-auth OurKith via PKCE flow (see Quick Token Refresh), then fix-dude

Why no credentials.json fallback? The old fix-dude fell back to credentials.json when refresh failed, which could overwrite a fresh PKCE token with a stale one. Now fix-dude only updates OurKith on successful refresh — if refresh fails, whatever's already in auth-profiles.json stays untouched.


Maintaining fix-dude

fix-dude hardcodes the canonical rotation order. Any time you add, remove, or reorder profiles, update this line in ~/.zshrc:

d['order']['anthropic'] = ['anthropic:mike@OurKith','anthropic:mike@anthemx.ca',...]

Rules

  • All profiles must be type: "token" — never type: "oauth"
  • OurKith must always be first (primary account, fix-dude refreshes its token)
  • Keep fix-dude order in sync with auth-profiles.json
  • If a profile 403s, it cascades ALL profiles to cooldown. Remove the bad profile immediately.

Checking What's Active

python3 -c "
import json, datetime
d = json.load(open('/Users/agentclaw/.openclaw/agents/main/agent/auth-profiles.json'))
print('lastGood:', d['lastGood']['anthropic'])
print('order:', d['order']['anthropic'])
for k,v in sorted(d.get('usageStats',{}).items(), key=lambda x: x[1].get('lastUsed',0), reverse=True):
    if 'anthropic' in k:
        ts = v.get('lastUsed', 0)
        dt = datetime.datetime.fromtimestamp(ts/1000).strftime('%H:%M:%S') if ts else 'never'
        print(f'  {k}: lastUsed={dt}, errors={v.get(\"errorCount\",0)}')
"

Diagnosing Issues

Check gateway error log

grep -i "error\|403\|401\|auth\|fail" /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log | tail -20

Kill gateway properly

launchctl bootout gui/$UID/ai.openclaw.gateway 2>/dev/null
kill -9 $(lsof -ti :18789) 2>/dev/null
sleep 3
openclaw gateway install --force

Do NOT just kill -9 without launchctl bootout first — supervised process respawns with stale tokens.


Error Reference

Error Cause Fix
Refresh failed: ... — keeping existing OurKith token (fix-dude) Refresh token expired/revoked No action — existing PKCE token preserved
403 OAuth authentication is currently not allowed Profile is type: "oauth" Fix to type: "token", run fix-dude
403 cascades all profiles to cooldown One bad profile Remove bad profile from order, fix-dude
invalid x-api-key (API plan) Token expired Re-auth or remove from rotation
invalid x-api-key (Max plan) Expected Max tokens only work through gateway
All models failed (rate_limit) All profiles exhausted Wait for reset or add accounts
Gateway survives kill with stale tokens Missing launchctl bootout Use launchctl bootout first
invalid_grant during exchange Auth code expired (~60s) Re-run pkce_start.py and redo flow
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment