- Setup wizard with auto-detection of OpenClaw paths and Claude CLI - Token sync watcher (inotifywait) for real-time credential updates - Auto-refresh trigger timer that runs Claude CLI every 30 min - Supports Claude CLI in Docker container or on host - Temporary ANTHROPIC_BASE_URL override for container environments - Anthropic model configuration for OpenClaw - Auth profile management (fixes key vs access field) - Systemd services and timers for both sync and trigger - Comprehensive documentation and troubleshooting guides - Re-authentication notification system Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
126 lines
4.2 KiB
Bash
Executable File
126 lines
4.2 KiB
Bash
Executable File
#!/bin/bash
|
|
# refresh-claude-token.sh — Directly refreshes Claude Max OAuth token via Anthropic API
|
|
# Fallback method for servers without a Claude CLI container doing auto-refresh
|
|
# Run via systemd timer every 6 hours
|
|
#
|
|
# Endpoint: POST https://console.anthropic.com/v1/oauth/token
|
|
# Client ID: 9d1c250a-e61b-44d9-88ed-5944d1962f5e (Claude Code public OAuth client)
|
|
|
|
set -euo pipefail
|
|
|
|
# --- Configuration (can be overridden via env vars or setup.sh substitution) ---
|
|
CLAUDE_CREDS_FILE="${CLAUDE_CREDS_FILE:-@@CLAUDE_CREDS_FILE@@}"
|
|
OPENCLAW_OAUTH_FILE="${OPENCLAW_OAUTH_FILE:-@@OPENCLAW_OAUTH_FILE@@}"
|
|
OPENCLAW_ENV_FILE="${OPENCLAW_ENV_FILE:-@@OPENCLAW_ENV_FILE@@}"
|
|
COMPOSE_DIR="${COMPOSE_DIR:-@@COMPOSE_DIR@@}"
|
|
TOKEN_URL="https://console.anthropic.com/v1/oauth/token"
|
|
CLIENT_ID="9d1c250a-e61b-44d9-88ed-5944d1962f5e"
|
|
LOG_PREFIX="[refresh-claude-token]"
|
|
|
|
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $LOG_PREFIX $*"; }
|
|
error() { echo "$(date '+%Y-%m-%d %H:%M:%S') $LOG_PREFIX ERROR: $*" >&2; }
|
|
|
|
# Check credentials file exists (source of current refresh token)
|
|
if [ ! -f "$CLAUDE_CREDS_FILE" ]; then
|
|
error "Credentials file not found: $CLAUDE_CREDS_FILE"
|
|
exit 1
|
|
fi
|
|
|
|
# Extract current refresh token
|
|
REFRESH_TOKEN=$(python3 -c "
|
|
import json, sys
|
|
try:
|
|
with open('$CLAUDE_CREDS_FILE') as f:
|
|
data = json.load(f)
|
|
print(data['claudeAiOauth']['refreshToken'])
|
|
except Exception as e:
|
|
print(f'ERROR: {e}', file=sys.stderr)
|
|
sys.exit(1)
|
|
")
|
|
|
|
if [ -z "$REFRESH_TOKEN" ]; then
|
|
error "No refresh token found in $CLAUDE_CREDS_FILE"
|
|
exit 1
|
|
fi
|
|
|
|
log "Refreshing token..."
|
|
|
|
# Call Anthropic's OAuth token endpoint
|
|
RESPONSE=$(curl -s -X POST "$TOKEN_URL" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"grant_type\":\"refresh_token\",\"refresh_token\":\"$REFRESH_TOKEN\",\"client_id\":\"$CLIENT_ID\"}")
|
|
|
|
# Parse response and write to all credential locations
|
|
python3 -c "
|
|
import json, sys, time, os, re
|
|
|
|
resp = json.loads('''$RESPONSE''')
|
|
|
|
if 'access_token' not in resp:
|
|
print('$LOG_PREFIX ERROR: Refresh failed:', json.dumps(resp), file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
access_token = resp['access_token']
|
|
refresh_token = resp['refresh_token']
|
|
expires_at = int(time.time() * 1000) + (resp['expires_in'] * 1000)
|
|
scopes = resp.get('scope', 'user:inference user:mcp_servers user:profile user:sessions:claude_code').split(' ')
|
|
|
|
# 1. Write Claude CLI credentials format (keeps refresh token for next run)
|
|
claude_creds = {
|
|
'claudeAiOauth': {
|
|
'accessToken': access_token,
|
|
'refreshToken': refresh_token,
|
|
'expiresAt': expires_at,
|
|
'scopes': scopes,
|
|
'subscriptionType': 'max',
|
|
'rateLimitTier': 'default_claude_max_5x'
|
|
}
|
|
}
|
|
with open('$CLAUDE_CREDS_FILE', 'w') as f:
|
|
json.dump(claude_creds, f)
|
|
print(f'Updated $CLAUDE_CREDS_FILE')
|
|
|
|
# 2. Update ANTHROPIC_OAUTH_TOKEN in .env
|
|
with open('$OPENCLAW_ENV_FILE') as f:
|
|
env = f.read()
|
|
if 'ANTHROPIC_OAUTH_TOKEN' in env:
|
|
env = re.sub(r'ANTHROPIC_OAUTH_TOKEN=.*', f'ANTHROPIC_OAUTH_TOKEN=\"{access_token}\"', env)
|
|
else:
|
|
env = env.rstrip('\n') + f'\nANTHROPIC_OAUTH_TOKEN=\"{access_token}\"\n'
|
|
with open('$OPENCLAW_ENV_FILE', 'w') as f:
|
|
f.write(env)
|
|
print(f'Updated $OPENCLAW_ENV_FILE')
|
|
|
|
# 3. Write OpenClaw oauth.json (field-mapped)
|
|
openclaw_oauth = {
|
|
'anthropic': {
|
|
'access': access_token,
|
|
'refresh': refresh_token,
|
|
'expires': expires_at,
|
|
'scopes': scopes,
|
|
'subscriptionType': 'max',
|
|
'rateLimitTier': 'default_claude_max_5x'
|
|
}
|
|
}
|
|
os.makedirs(os.path.dirname('$OPENCLAW_OAUTH_FILE'), exist_ok=True)
|
|
with open('$OPENCLAW_OAUTH_FILE', 'w') as f:
|
|
json.dump(openclaw_oauth, f)
|
|
print(f'Updated $OPENCLAW_OAUTH_FILE')
|
|
|
|
expires_hours = resp['expires_in'] // 3600
|
|
account = resp.get('account', {}).get('email_address', 'unknown')
|
|
print(f'OK: account={account}, expires in {expires_hours}h, token={access_token[:20]}...')
|
|
"
|
|
|
|
if [ $? -ne 0 ]; then
|
|
error "Token refresh failed"
|
|
exit 1
|
|
fi
|
|
|
|
# 4. Recreate gateway container (down/up, NOT restart)
|
|
cd "$COMPOSE_DIR"
|
|
log "Recreating gateway container..."
|
|
docker compose down openclaw-gateway 2>&1 | sed "s/^/$LOG_PREFIX /"
|
|
docker compose up -d openclaw-gateway 2>&1 | sed "s/^/$LOG_PREFIX /"
|
|
log "Gateway recreated with new token"
|