- 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>
4.1 KiB
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:
- Checks
[API:auth] OAuth token check— reads.credentials.json - 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
- Sends
- 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-proxycontainer, writes to its own.credentials.json - OpenClaw gateway runs in
openclaw-openclaw-gateway-1, reads fromoauth.jsonand 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, thenmv)- We watch the directory not the file, because atomic renames create a new inode
When the file changes:
- Read
accessToken,refreshToken,expiresAtfrom Claude CLI format - Map fields:
accessToken->access,refreshToken->refresh,expiresAt->expires - Write to
oauth.json(for gateway'smergeOAuthFileIntoStore()) - Update
ANTHROPIC_OAUTH_TOKENin.env - 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:
- Reads current refresh token from
.credentials.json - Calls Anthropic's token endpoint directly
- Writes new tokens to all credential locations
- 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.