#!/bin/bash # trigger-claude-refresh.sh — Triggers Claude CLI to refresh OAuth token when near expiry # # Runs via systemd timer every 30 minutes. # Checks token expiry, triggers CLI only when needed. # The existing sync-oauth-token.sh (inotifywait) handles syncing to OpenClaw. # # Supports two modes: # Container mode: Claude CLI runs inside a Docker container # Host mode: Claude CLI is installed directly on the system set -uo pipefail CREDS_FILE="@@CREDS_FILE@@" REAUTH_FLAG="@@REAUTH_FLAG@@" CLI_MODE="@@CLI_MODE@@" CLI_CONTAINER="@@CLI_CONTAINER@@" CLI_BASE_URL_OVERRIDE="@@CLI_BASE_URL_OVERRIDE@@" LOG_PREFIX="[trigger-refresh]" THRESHOLD_HOURS=1.5 TIMEOUT_SECONDS=60 log() { echo "$LOG_PREFIX $*"; } log_err() { echo "$LOG_PREFIX ERROR: $*" >&2; } # Check prerequisites if [ ! -f "$CREDS_FILE" ]; then log_err "Credentials file not found: $CREDS_FILE" exit 1 fi if [ "$CLI_MODE" = "container" ]; then if ! docker ps --filter "name=$CLI_CONTAINER" --format '{{.Names}}' | grep -q "$CLI_CONTAINER"; then log_err "Container $CLI_CONTAINER is not running" exit 1 fi else if ! command -v claude &>/dev/null; then log_err "Claude CLI not found on system" exit 1 fi fi # Read expiry and decide whether to trigger REMAINING=$(python3 -c " import json, time with open('$CREDS_FILE') as f: d = json.load(f) expires = d.get('claudeAiOauth', {}).get('expiresAt', 0) remaining = (expires / 1000 - time.time()) / 3600 print(f'{remaining:.2f}') ") log "Token expires in ${REMAINING}h (threshold: ${THRESHOLD_HOURS}h)" # Compare as integers (multiply by 100 to avoid bash float issues) REMAINING_X100=$(python3 -c "print(int(float('$REMAINING') * 100))") THRESHOLD_X100=$(python3 -c "print(int(float('$THRESHOLD_HOURS') * 100))") if [ "$REMAINING_X100" -gt "$THRESHOLD_X100" ]; then log "Token still fresh, nothing to do" exit 0 fi log "Token near expiry, triggering Claude CLI refresh..." # Record mtime BEFORE MTIME_BEFORE=$(stat -c %Y "$CREDS_FILE" 2>/dev/null || stat -f %m "$CREDS_FILE" 2>/dev/null) # Trigger Claude CLI if [ "$CLI_MODE" = "container" ]; then if [ "$CLI_BASE_URL_OVERRIDE" = "true" ]; then CLI_OUTPUT=$(timeout "$TIMEOUT_SECONDS" docker exec \ -e ANTHROPIC_BASE_URL=https://api.anthropic.com \ "$CLI_CONTAINER" claude -p "say ok" --no-session-persistence 2>&1) else CLI_OUTPUT=$(timeout "$TIMEOUT_SECONDS" docker exec \ "$CLI_CONTAINER" claude -p "say ok" --no-session-persistence 2>&1) fi else CLI_OUTPUT=$(timeout "$TIMEOUT_SECONDS" claude -p "say ok" --no-session-persistence 2>&1) fi CLI_EXIT=$? if [ "$CLI_EXIT" -eq 124 ]; then log_err "CLI command timed out after ${TIMEOUT_SECONDS}s" fi log "CLI exit code: $CLI_EXIT, output: $CLI_OUTPUT" # Record mtime AFTER sleep 2 MTIME_AFTER=$(stat -c %Y "$CREDS_FILE" 2>/dev/null || stat -f %m "$CREDS_FILE" 2>/dev/null) if [ "$MTIME_BEFORE" != "$MTIME_AFTER" ]; then log "Token refreshed successfully (mtime changed: $MTIME_BEFORE -> $MTIME_AFTER)" if [ -f "$REAUTH_FLAG" ]; then rm -f "$REAUTH_FLAG" log "Cleared previous REAUTH_NEEDED flag" fi exit 0 else log_err "Token refresh FAILED — credentials.json was not updated" log_err "Re-authentication may be required (refresh token expired or subscription issue)" echo "Re-authentication needed at $(date -u '+%Y-%m-%dT%H:%M:%SZ')" > "$REAUTH_FLAG" log_err "Wrote $REAUTH_FLAG" exit 1 fi