openclaw_oauth_sync/docs/HOW-TOKEN-REFRESH-WORKS.md
shamid202 22731fff60 Add complete OAuth token refresh and sync solution
- 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>
2026-02-27 01:51:18 +07:00

4.1 KiB

How Token Refresh Works

Anthropic OAuth Token Lifecycle

Claude Max subscriptions use OAuth tokens for API authentication.

  • Access token (sk-ant-oat01-...): Used for API requests, expires in ~8 hours
  • Refresh token (sk-ant-ort01-...): Used to get new access tokens, long-lived
  • Token endpoint: POST https://console.anthropic.com/v1/oauth/token
  • Client ID: 9d1c250a-e61b-44d9-88ed-5944d1962f5e (Claude Code public OAuth client)

How Claude Code CLI Refreshes Tokens

Claude Code CLI has built-in token refresh. Before each API request:

  1. Checks [API:auth] OAuth token check — reads .credentials.json
  2. If access token is expired or near expiry:
    • Sends grant_type: "refresh_token" to Anthropic's token endpoint
    • Gets back new access_token, refresh_token, expires_in
    • Writes updated credentials back to .credentials.json
  3. Uses the (possibly refreshed) access token for the API request

The relevant function in Claude Code's minified source (cli.js):

// Simplified from minified source
async function refreshToken(refreshToken, scopes) {
    const params = {
        grant_type: "refresh_token",
        refresh_token: refreshToken,
        client_id: CLIENT_ID,
        scope: scopes.join(" ")
    };
    const response = await axios.post(TOKEN_URL, params, {
        headers: { "Content-Type": "application/json" }
    });
    return {
        accessToken: response.data.access_token,
        refreshToken: response.data.refresh_token || refreshToken,
        expiresAt: Date.now() + response.data.expires_in * 1000,
        scopes: response.data.scope.split(" ")
    };
}

Why We Need a Sync Service

The problem is that Claude Code CLI and OpenClaw are separate processes in separate containers:

  • Claude Code CLI runs in claude-proxy container, writes to its own .credentials.json
  • OpenClaw gateway runs in openclaw-openclaw-gateway-1, reads from oauth.json and env vars

They don't share the same credential files. So when Claude CLI refreshes the token, OpenClaw doesn't know about it.

The Sync Approach (inotifywait)

We use inotifywait from inotify-tools to watch for file changes in real-time:

inotifywait -q -e close_write,moved_to "$WATCH_DIR"
  • close_write: Fires when a file is written and closed (normal writes)
  • moved_to: Fires when a file is moved into the directory (atomic writes: write to temp, then mv)
  • We watch the directory not the file, because atomic renames create a new inode

When the file changes:

  1. Read accessToken, refreshToken, expiresAt from Claude CLI format
  2. Map fields: accessToken -> access, refreshToken -> refresh, expiresAt -> expires
  3. Write to oauth.json (for gateway's mergeOAuthFileIntoStore())
  4. Update ANTHROPIC_OAUTH_TOKEN in .env
  5. Recreate gateway container (docker compose down/up) to reload env vars

The Fallback Approach (Timer)

If inotifywait is unavailable, a systemd timer runs every 6 hours:

  1. Reads current refresh token from .credentials.json
  2. Calls Anthropic's token endpoint directly
  3. Writes new tokens to all credential locations
  4. Recreates gateway container

This is less responsive (up to 6-hour delay) but works without inotify.

Field Mapping

Claude CLI (.credentials.json) OpenClaw (oauth.json)
claudeAiOauth.accessToken anthropic.access
claudeAiOauth.refreshToken anthropic.refresh
claudeAiOauth.expiresAt anthropic.expires
claudeAiOauth.scopes anthropic.scopes

Timeline

T=0h    Token issued (access + refresh)
T=7.5h  Claude CLI detects token nearing expiry
T=7.5h  CLI calls refresh endpoint, gets new tokens
T=7.5h  CLI writes new .credentials.json
T=7.5h  inotifywait detects change (< 1 second)
T=7.5h  sync-oauth-token.sh syncs to oauth.json + .env
T=7.5h  Gateway recreated with fresh token
T=8h    Old token would have expired (but we already refreshed)

The entire sync happens within seconds of the CLI refresh, well before the old token expires.