musistudio/claude-code-router · error · Error

Provider payload must be a JSON object.

Error message

Provider payload must be a JSON object.

What it means

readPayloadRecord decodes the payload= deep-link parameter (raw JSON if it starts with '{', otherwise base64url) and requires the decoded value to be a non-null, non-array object. Arrays, strings, numbers, or null after JSON.parse trigger this error.

Source

Thrown at packages/core/src/contracts/deep-link.ts:420

        return JSON.parse(paramValue);
      } catch {
        return paramValue;
      }
    }
  }
  return undefined;
}

function readPayloadRecord(params: URLSearchParams): Record<string, unknown> | undefined {
  const value = firstStringParam(params, ["payload"]);
  if (!value) {
    return undefined;
  }

  const jsonText = value.trim().startsWith("{") ? value : decodeBase64Url(value);
  const parsed = JSON.parse(jsonText) as unknown;
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
    throw new Error("Provider payload must be a JSON object.");
  }
  return parsed as Record<string, unknown>;
}

function decodeBase64Url(value: string): string {
  const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
  const binary = typeof atob === "function"
    ? atob(padded)
    : Buffer.from(padded, "base64").toString("binary");
  const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

function firstStringParam(params: URLSearchParams, names: string[]): string | undefined {
  for (const name of names) {
    const value = params.get(name);
    if (typeof value === "string" && value.trim()) {
      return value.trim();

View on GitHub (pinned to 99f24806c6)

Solutions

  1. Make payload a JSON object: {...} (base64url of an object if not inline)
  2. Don't wrap in an array or quotes; encode with base64url(JSON.stringify(obj)) only when not starting with '{'
  3. Test decoding locally: JSON.parse(Buffer.from(payload,'base64url').toString()) before shipping the link

Example fix

// before
ccr://provider/install?payload=W1sibmFtZSJdXQ (decodes to [["name"]])
// after
ccr://provider/install?payload=eyJuYW1lIjoiYWNtZSJ9 (decodes to {"name":"acme"})
Defensive patterns

Strategy: type-guard

Validate before calling

const text = param.startsWith("{") ? param : Buffer.from(param, "base64url").toString(); const v = JSON.parse(text); if (typeof v !== "object" || v === null || Array.isArray(v)) return;

Type guard

const isPayloadRecord = (v: unknown): v is Record<string, unknown> =>
  !!v && typeof v === "object" && !Array.isArray(v);

Try / catch

try { parseProviderDeepLinkPayload(url); } catch (e) { if (e instanceof Error && e.message === "Provider payload must be a JSON object.") return showPayloadHelp(); throw e; }

Prevention

When it happens

Trigger: A payload= param whose decoded JSON is an array (e.g. [{...}]) or a scalar ("abc", 42, null), or a base64url blob that decodes to non-object JSON.

Common situations: Encoding a list of providers instead of a single record; double-encoding leaving a quoted JSON string; hand-crafted base64 that decodes to malformed JSON shape.

Related errors


AI-assisted analysis of musistudio/claude-code-router@99f24806c6 (2026-08-27). Data as JSON: /api/errors/e9142c9a6c422eac. Report an issue: GitHub.