{"record":{"id":"a12644f31a518573","repo":"JuliusBrussee/caveman","slug":"opencode-inline-configuration-must-be-a-json-object","errorCode":null,"errorMessage":"opencode inline configuration must be a JSON object","messagePattern":"opencode inline configuration must be a JSON object","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":9400,"sourceCode":"    let rendered = renderDeep(content, renderedGw, env, renderOptions);\n    if (agent.id === \"kilo\" && mcpMode === \"auto\") {\n      const ownedMcp = ownedMcpRegistration(agent.id, agentArgs);\n      if (ownedMcp) {\n        // Project exact owned registration into Kilo's highest user-controlled\n        // layer. Route preflight rejects active org/managed sources that load\n        // afterward, so no later known policy can silently disable recovery.\n        rendered = deepMerge(rendered, { mcp: { caveman: kiloMcpEntry(ownedMcp) } });\n      }\n    }\n    if (agent.id === \"opencode\" && process.env[inj.env_var]) {\n      // OpenCode treats inline JSONC as its own configuration layer. Replacing\n      // that layer loses the user's model, account, permissions and MCP servers.\n      // Preserve native {env:...}/{file:...} references for OpenCode to resolve\n      // in the same context; only our routing fields take precedence.\n      let original: unknown;\n      try { original = parseJsonc(process.env[inj.env_var]!); }\n      catch { throw new Error(\"cannot preserve opencode inline configuration; launching with the original configuration is required\"); }\n      if (!isPlainObject(original)) throw new Error(\"opencode inline configuration must be a JSON object\");\n      rendered = deepMerge(original, rendered);\n    }\n    env[inj.env_var] = JSON.stringify(rendered);\n  } else if (inj.method === \"config-file\") {\n    try {\n      applyConfigFileInjection(env, agent, inj, renderedGw, gw, mcpMode, agentArgs, upstreams);\n    } catch (e) {\n      // OpenClaw ignores the generic base-URL union. Config injection is its only\n      // provider redirect, so a failed/missing route must abort the wrapped path;\n      // spawnWrapped then launches direct with an explicit warning.\n      if (agent.id === \"openclaw\" || agent.id === \"qwen\") throw e;\n      process.stderr.write(`caveman: ${agent.id} config-file injection failed; using generic env wrap (${(e as Error).message})\\n`);\n    }\n  }\n  // Qwen 0.22 discovers MCP servers in the background by default. Its first\n  // request can therefore omit caveman_retrieve even though our durable marker\n  // says recovery is installed. Proxy compression must never outrun recovery,\n  // so marker-backed Qwen wraps use Qwen's compatibility switch to finish MCP","sourceCodeStart":9382,"sourceCodeEnd":9418,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L9382-L9418","documentation":"After successfully parsing OpenCode's existing inline configuration as JSONC, Caveman requires the result to be a JSON object (a plain record of keys) because it deep-merges its routing fields into it. If the parsed value is an array, string, number, or null, there is nothing to merge into and the configuration layer would be structurally invalid for OpenCode, so the wrapper throws with this message.","triggerScenarios":"process.env[inj.env_var] contains JSONC that parses successfully but whose top-level value is not a plain object — e.g. '[\"a\",\"b\"]', '\"just a string\"', '42', or 'null'.","commonSituations":"Storing an array of MCP servers directly in the inline config var instead of an object with an mcpServers key; a script that serializes the wrong variable; hand-editing the config down to a bare value.","solutions":["Rewrite the inline config so its top level is a JSON object: {\"model\": ..., \"mcpServers\": {...}} rather than an array or scalar.","Validate with node -e \"const v=JSON.parse(process.env.OPENCODE_CONFIG); if (v===null || typeof v!=='object' || Array.isArray(v)) process.exit(1)\" before launching.","Unset the variable and configure OpenCode via its config file if the inline value cannot be reshaped."],"exampleFix":"// before\nexport OPENCODE_CONFIG='[\"server-a\",\"server-b\"]'   // array, not object\n// after\nexport OPENCODE_CONFIG='{\"mcpServers\":{\"a\":{\"command\":\"server-a\"}}}'","handlingStrategy":"type-guard","validationCode":"const parsed = JSON.parse(process.env.OPENCODE_CONFIG);\nif (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n  console.error(\"OpenCode inline config must be a JSON object at the top level.\");\n  process.exit(1);\n}","typeGuard":"function isPlainObject(v) {\n  return v !== null && typeof v === \"object\" && !Array.isArray(v);\n}","tryCatchPattern":"try {\n  caveman.launch({ agent: \"opencode\" });\n} catch (e) {\n  if (e instanceof Error && e.message === \"opencode inline configuration must be a JSON object\") {\n    console.error(\"Wrap the inline config value in a top-level JSON object.\");\n  } else throw e;\n}","preventionTips":["Always structure inline OpenCode config as {key: value, ...}; never assign arrays or scalars directly.","Add a schema/type check in scripts that generate the inline config.","Keep MCP server lists under an mcpServers object key rather than at the top level as an array."],"tags":["opencode","json","configuration","schema","type-error"],"backgroundTag":"config-type-mismatch","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}