Covers: adding accounts, refreshing tokens, rotation order, and maintaining fix-dude.
| 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.jsonhas anauthsection but the main agent does not use it. It reads exclusively fromauth-profiles.json.
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 usetype: "oauth"— using Bearer auth causes HTTP 403 "OAuth authentication is currently not allowed for this organization" from Anthropic's API. Allsk-ant-oat01-*tokens must be sent asx-api-keyviatype: "token".
| # | 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.
This is the easiest way to refresh expired tokens. No manual curl or Python exchange needed.
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"))
PYEOFFor each account that needs refreshing:
- In a separate terminal, run
python3 /tmp/pkce_start.py - Open the printed URL in your browser
- Sign in as the target account
- Copy the
code#statestring from the callback - 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.jsonwith the new token - Confirm success
⚠️ Auth codes expire in ~60 seconds. Paste into Claude Code promptly after getting the code.
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 --forceOr just run source ~/.zshrc && fix-dude which also clears cooldowns.
python3 /tmp/pkce_start.pyOpen the printed URL, sign in as the new account, copy the code#state string.
Paste into Claude Code:
newaccount@example.com CODE#STATE
Claude handles the exchange and adds the profile to auth-profiles.json.
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.
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:
- Move new profile to front of rotation order
- Restart gateway:
source ~/.zshrc && fix-dude - Send a message through Discord
- Check usageStats shows
lastUsedwitherrorCount: 0
Add new profile to order in ~/.zshrc, then: source ~/.zshrc && fix-dude
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
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.
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.
- Tries to refresh OurKith OAuth token via
https://console.anthropic.com/v1/oauth/token - If refresh succeeds, writes new token to auth-profiles.json and syncs to credentials.json
- 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)
- Resets rotation order to canonical list
- Clears all cooldowns, failureCounts, and errorCounts
- Sets
lastGoodtoanthropic:mike@OurKith - Stops gateway via
launchctl bootout+kill -9, restarts withopenclaw gateway install --force
- 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
source ~/.zshrc && fix-dudeCannot be run from inside Claude Code. Use a separate terminal.
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."
}| 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.
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',...]- All profiles must be
type: "token"— nevertype: "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.
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)}')
"grep -i "error\|403\|401\|auth\|fail" /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log | tail -20launchctl bootout gui/$UID/ai.openclaw.gateway 2>/dev/null
kill -9 $(lsof -ti :18789) 2>/dev/null
sleep 3
openclaw gateway install --forceDo NOT just
kill -9withoutlaunchctl bootoutfirst — supervised process respawns with stale tokens.
| 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 |