JuliusBrussee/caveman · error

provider must be a JSON object; refusing to overwrite it

Error message

${configPath} provider must be a JSON object; refusing to overwrite it

What it means

`opencodeNativeMutations()` modifies ~/.config/opencode/opencode.json to add Caveman provider routes. Before writing, it validates that an existing top-level `provider` key is a plain JSON object; any other type (string, array, null) aborts the mutation so the CLI never clobbers user configuration it cannot merge.

Solutions

  1. Open opencode.json (path shown in the message) and change `provider` to a JSON object
  2. Back up the file before editing: cp opencode.json opencode.json.bak
  3. If provider is an array, convert entries to object properties keyed by provider id
  4. Re-run `caveman setup` after fixing the shape

Example fix

// before: ~/.config/opencode/opencode.json
{ "provider": ["openai", "anthropic"] }
// after
{ "provider": { "openai": {}, "anthropic": {} } }
Defensive patterns

Strategy: type-guard

Validate before calling

// Pre-flight on opencode.json:
const cfg = JSON.parse(fs.readFileSync(opencodeConfigPath, 'utf8'));
if (cfg.provider !== undefined && (typeof cfg.provider !== 'object' || cfg.provider === null || Array.isArray(cfg.provider))) {
  console.error(`Fix ${opencodeConfigPath}: provider must be an object`);
}

Type guard

function isProviderMap(v: unknown): v is Record<string, unknown> {
  return typeof v === 'object' && v !== null && !Array.isArray(v);
}

Try / catch

try {
  await cavemanSetup(['opencode']);
} catch (e) {
  if (String(e.message).includes('provider must be a JSON object')) {
    // fix provider shape in opencode.json, then retry
  }
  throw e;
}

Prevention

When it happens

Trigger: Running native opencode setup when the parsed opencode.json has `provider` defined but not as a non-null plain object (`typeof root.provider !== "object" || null || Array.isArray`).

Common situations: Hand-edited opencode.json where provider was written as a string or list; a config generator emitted provider: null as a placeholder; a merge/sync tool flattened the provider map; copy-paste of an invalid snippet.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/393277a336cae768. Report an issue: GitHub.

Appendix: source

Thrown at packages/cli/src/index.ts:7327

    call("PreCompact", { session_id: input.sessionID });
    const stable = sessionContext(input.sessionID);
    if (stable) output.context.push(stable);
  },
  dispose: async () => {
    for (const sessionID of contexts.keys()) call("SessionEnd", { session_id: sessionID });
    contexts.clear();
    pending.clear();
  },
});
`;
}

function opencodeNativeMutations(gw: string, mcpBinary: string): NativeMutation[] {
  const configPath = join(homedir(), ".config", "opencode", "opencode.json");
  const before = fileBytes(configPath);
  const root = parseJsonFileObject(configPath, before);
  if (root.provider !== undefined && (typeof root.provider !== "object" || root.provider === null || Array.isArray(root.provider))) {
    throw new Error(`${configPath} provider must be a JSON object; refusing to overwrite it`);
  }
  if (root.mcp !== undefined && (typeof root.mcp !== "object" || root.mcp === null || Array.isArray(root.mcp))) {
    throw new Error(`${configPath} mcp must be a JSON object; refusing to overwrite it`);
  }
  const providers = root.provider && typeof root.provider === "object" && !Array.isArray(root.provider) ? root.provider as Record<string, unknown> : {};
  const previousRoutes: Record<string, unknown> = {};
  const base = appendUrlPath(gw, "/w/opencode");
  const routes = { openai: appendUrlPath(base, "/openai/v1"), anthropic: appendUrlPath(base, "/anthropic/v1") };
  for (const [providerID, route] of Object.entries(routes)) {
    const provider = providers[providerID] && typeof providers[providerID] === "object" && !Array.isArray(providers[providerID]) ? providers[providerID] as Record<string, unknown> : {};
    const options = provider.options && typeof provider.options === "object" && !Array.isArray(provider.options) ? provider.options as Record<string, unknown> : {};
    previousRoutes[providerID] = options.baseURL ?? null;
    options.baseURL = route;
    provider.options = options;
    providers[providerID] = provider;
  }
  root.provider = providers;
  const mcp = root.mcp && typeof root.mcp === "object" && !Array.isArray(root.mcp) ? root.mcp as Record<string, unknown> : {};

View on GitHub (pinned to 3ee70a1026)