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
- Open opencode.json (path shown in the message) and change `provider` to a JSON object
- Back up the file before editing: cp opencode.json opencode.json.bak
- If provider is an array, convert entries to object properties keyed by provider id
- 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
- Keep `provider` as a keyed object in opencode.json (providerID -> config)
- Back up opencode.json before hand edits
- Validate config against opencode's schema after using generators or sync tools
- Avoid placeholder nulls in generated config
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
- mcp must be a JSON object; refusing to overwrite it
- env must be a JSON object; refusing to overwrite it
- mcpServers must be a JSON object; refusing to overwrite it
- mcpServers must be a JSON object; refusing to overwrite it
- : MCP ownership journal failed; native config may already…
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)