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
- Set CLAUDE_MEM_CLOUD_SYNC_HUB_URL to the hub base URL (e.g. https://sync.example.com).
- Confirm the env var is actually visible to the worker process (export it, restart the service).
- Validate hubUrl before constructing: if (!url?.trim()) skip/queue client creation.
- 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
- Document required env vars and check them at boot.
- Trim/normalize env values before passing to constructors.
- Fail fast with a clear message instead of passing undefined through.
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
- ActiveServerQueueManager requires…
- Backfill failed
- canonical content
- Chroma unavailable before write; leaving documents unsynced
- CLAUDE_MEM_QUEUE_ENGINE is not "bullmq"
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)