thedotmack/claude-mem · error

SyncClient requires a non-empty hubUrl…

Error message

SyncClient requires a non-empty hubUrl (CLAUDE_MEM_CLOUD_SYNC_HUB_URL)

What it means

The SyncClient constructor trims and strips trailing slashes from options.hubUrl and requires a non-empty result, naming the CLAUDE_MEM_CLOUD_SYNC_HUB_URL env var since that is the usual source. Without a hub URL the realtime pull loop has nowhere to connect, so construction fails closed. A missing deviceId similarly throws immediately after.

Solutions

  1. Set CLAUDE_MEM_CLOUD_SYNC_HUB_URL to the hub base URL (e.g. https://sync.example.com).
  2. Confirm the env var is actually visible to the worker process (export it, restart the service).
  3. Validate hubUrl before constructing: if (!url?.trim()) skip/queue client creation.
  4. Also ensure deviceId is provided (the very next check throws otherwise).

Example fix

// before
const client = new SyncClient(apply, { hubUrl: process.env.CLAUDE_MEM_CLOUD_SYNC_HUB_URL, deviceId });
// after
const hubUrl = process.env.CLAUDE_MEM_CLOUD_SYNC_HUB_URL;
if (!hubUrl?.trim()) throw new Error('CLAUDE_MEM_CLOUD_SYNC_HUB_URL must be set');
const client = new SyncClient(apply, { hubUrl, deviceId });
Defensive patterns

Strategy: validation

Validate before calling

const hubUrl = process.env.CLAUDE_MEM_CLOUD_SYNC_HUB_URL;
if (!hubUrl || !hubUrl.trim()) throw new Error('CLAUDE_MEM_CLOUD_SYNC_HUB_URL is required');
const client = new SyncClient(apply, { hubUrl, deviceId });

Try / catch

try { client = new SyncClient(apply, opts); } catch (e) { if (e.message.includes('non-empty hubUrl')) { /* surface missing-env-var config error to user */ } }

Prevention

When it happens

Trigger: new SyncClient(apply, { hubUrl: undefined | '' | ' ', deviceId }) — typically when CLAUDE_MEM_CLOUD_SYNC_HUB_URL is unset/empty and the caller passes it through unchecked.

Common situations: Env var not exported in shell/service context; settings.json lacks the sync hub URL; value set only to whitespace; constructing SyncClient in tests without options.

Understand the failure class

Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/9c739822809c3287. Report an issue: GitHub.

Appendix: source

Thrown at src/services/sync/SyncClient.ts:256

  private backoffMs = 0;
  private failStreak = 0;
  private failCursor: string | null = null;

  // Advisory socket state (all of it disposable — prime directive #2).
  private socket: SyncSocketLike | null = null;
  private socketLive = false;
  private wsAttempts = 0;
  private pingTimer: ReturnType<typeof setInterval> | null = null;
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
  /** True while the hub says X-Sync-Mode: poll (kill switch tripped). */
  private pollModeOnly = false;
  /** True while the pull loop is suspended (socket torn down with it). */
  private suspended = false;

  constructor(apply: SyncApply, options: SyncClientOptions) {
    const hubUrl = (options.hubUrl ?? '').trim().replace(/\/+$/, '');
    if (!hubUrl) {
      throw new Error('SyncClient requires a non-empty hubUrl (CLAUDE_MEM_CLOUD_SYNC_HUB_URL)');
    }
    if (!options.deviceId) {
      // Same fail-closed posture as CloudSync/SyncApply: pulling without an
      // identity would mis-classify our own echoes.
      throw new Error('SyncClient requires a non-empty deviceId (use the CloudSync-resolved id)');
    }
    this.apply = apply;
    this.hubUrl = hubUrl;
    this.token = options.token ?? '';
    this.userId = options.userId ?? '';
    this.deviceId = options.deviceId;
    this.deviceName = (options.deviceName ?? '').trim().slice(0, 80);
    this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
    this.activePollMs = options.activePollMs ?? 30_000;
    this.idlePollMs = options.idlePollMs ?? 300_000;
    this.suspendAfterMs = options.suspendAfterMs ?? 3_600_000;
    this.pageLimit = options.pageLimit ?? 500;
    this.maxPagesPerCycle = options.maxPagesPerCycle ?? 40;

View on GitHub (pinned to d8bc9755e7)