{"record":{"id":"9c739822809c3287","repo":"thedotmack/claude-mem","slug":"syncclient-requires-a-non-empty-huburl-claude-mem","errorCode":null,"errorMessage":"SyncClient requires a non-empty hubUrl (CLAUDE_MEM_CLOUD_SYNC_HUB_URL)","messagePattern":"SyncClient requires a non-empty hubUrl \\(CLAUDE_MEM_CLOUD_SYNC_HUB_URL\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/services/sync/SyncClient.ts","lineNumber":256,"sourceCode":"  private backoffMs = 0;\n  private failStreak = 0;\n  private failCursor: string | null = null;\n\n  // Advisory socket state (all of it disposable — prime directive #2).\n  private socket: SyncSocketLike | null = null;\n  private socketLive = false;\n  private wsAttempts = 0;\n  private pingTimer: ReturnType<typeof setInterval> | null = null;\n  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n  /** True while the hub says X-Sync-Mode: poll (kill switch tripped). */\n  private pollModeOnly = false;\n  /** True while the pull loop is suspended (socket torn down with it). */\n  private suspended = false;\n\n  constructor(apply: SyncApply, options: SyncClientOptions) {\n    const hubUrl = (options.hubUrl ?? '').trim().replace(/\\/+$/, '');\n    if (!hubUrl) {\n      throw new Error('SyncClient requires a non-empty hubUrl (CLAUDE_MEM_CLOUD_SYNC_HUB_URL)');\n    }\n    if (!options.deviceId) {\n      // Same fail-closed posture as CloudSync/SyncApply: pulling without an\n      // identity would mis-classify our own echoes.\n      throw new Error('SyncClient requires a non-empty deviceId (use the CloudSync-resolved id)');\n    }\n    this.apply = apply;\n    this.hubUrl = hubUrl;\n    this.token = options.token ?? '';\n    this.userId = options.userId ?? '';\n    this.deviceId = options.deviceId;\n    this.deviceName = (options.deviceName ?? '').trim().slice(0, 80);\n    this.fetchImpl = options.fetchImpl ?? globalThis.fetch;\n    this.activePollMs = options.activePollMs ?? 30_000;\n    this.idlePollMs = options.idlePollMs ?? 300_000;\n    this.suspendAfterMs = options.suspendAfterMs ?? 3_600_000;\n    this.pageLimit = options.pageLimit ?? 500;\n    this.maxPagesPerCycle = options.maxPagesPerCycle ?? 40;","sourceCodeStart":238,"sourceCodeEnd":274,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/src/services/sync/SyncClient.ts#L238-L274","documentation":"Thrown by the SyncClient constructor when the hubUrl option (from CLAUDE_MEM_CLOUD_SYNC_HUB_URL) is empty or whitespace-only after trimming and trailing-slash stripping. SyncClient refuses to start without a hub endpoint because pulling without one would silently no-op. This is a fail-closed construction guard.","triggerScenarios":"`new SyncClient(apply, { hubUrl, ... })` where hubUrl is undefined, empty, or all whitespace. The env var CLAUDE_MEM_CLOUD_SYNC_HUB_URL is the documented source; if unset/blank the constructor throws synchronously before any network setup.","commonSituations":"Cloud sync enabled (e.g. CLAUDE_MEM_CLOUD_SYNC_ENABLED=true) but CLAUDE_MEM_CLOUD_SYNC_HUB_URL not set in the environment or ~/.claude-mem/settings.json, a typo/duplicate in the env var name, or the variable exported as empty. Common in fresh installs or CI where the env isn't provisioned.","solutions":["Set CLAUDE_MEM_CLOUD_SYNC_HUB_URL to the hub base URL (e.g. https://sync.example.com) in your shell config or ~/.claude-mem/settings.json.","If cloud sync is intentionally disabled, unset the enable flag (e.g. CLAUDE_MEM_CLOUD_SYNC_ENABLED=false) so SyncClient is not constructed.","Confirm the env var is exported in the process that runs the worker (print process.env.CLAUDE_MEM_CLOUD_SYNC_HUB_URL at startup).","Check for trailing whitespace or a missing export in dotfiles."],"exampleFix":"# before: CLAUDE_MEM_CLOUD_SYNC_ENABLED=true  (hub url missing -> throws)\n# after:  export CLAUDE_MEM_CLOUD_SYNC_HUB_URL=https://sync.example.com\n#         export CLAUDE_MEM_CLOUD_SYNC_ENABLED=true","handlingStrategy":"validation","validationCode":"function resolveHubUrl(env: NodeJS.ProcessEnv = process.env): string {\n  const url = (env.CLAUDE_MEM_CLOUD_SYNC_HUB_URL ?? '').trim().replace(/\\/+$/, '');\n  if (!url) throw new Error('CLAUDE_MEM_CLOUD_SYNC_HUB_URL must be set when cloud sync is enabled');\n  try { new URL(url); } catch { throw new Error('CLAUDE_MEM_CLOUD_SYNC_HUB_URL is not a valid URL: ' + url); }\n  return url;\n}\n// call before constructing SyncClient:","typeGuard":null,"tryCatchPattern":"let client: SyncClient;\ntry {\n  client = new SyncClient(apply, { hubUrl: resolveHubUrl(), deviceId, /* ... */ });\n} catch (e) {\n  if (e instanceof Error && e.message.includes('non-empty hubUrl')) {\n    logger.warn('SYNC', 'cloud sync disabled: hub url not configured'); // degrade gracefully\n    return;\n  }\n  throw e;\n}","preventionTips":["Always set CLAUDE_MEM_CLOUD_SYNC_HUB_URL when CLAUDE_MEM_CLOUD_SYNC_ENABLED is true.","Print the resolved env at worker startup to catch missing exports in CI.","If sync is optional, guard SyncClient construction behind the enable flag."],"tags":["sync","sync-client","config","environment","startup"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}