JuliusBrussee/caveman · error

cannot preserve opencode inline configuration; launching…

Error message

cannot preserve opencode inline configuration; launching with the original configuration is required

What it means

OpenCode treats the inline JSONC passed via its configuration environment variable as an entire configuration layer. To avoid destroying the user's model, account, permissions and MCP server settings, Caveman parses the pre-existing value and deep-merges its routing fields on top. If the existing value cannot be parsed as JSONC at all, the wrapper throws rather than silently overwriting a configuration it could not understand — relaunching with the original configuration is required.

Solutions

  1. Unset the inline config env var before launching so Caveman starts from a clean configuration layer: unset OPENCODE_CONFIG (or the variable named in the message context).
  2. Fix the value to be valid JSONC — validate it with a JSONC parser or `node -e "JSON.parse(...)"` after stripping comments — then relaunch.
  3. Move your OpenCode settings into OpenCode's on-disk config file instead of the inline env var, letting Caveman merge only its routing fields.

Example fix

// before
export OPENCODE_CONFIG='{"model": "gpt'   # truncated JSONC
// after
unset OPENCODE_CONFIG   # or export OPENCODE_CONFIG='{"model": "gpt", ...}' (valid JSONC)
Defensive patterns

Strategy: validation

Validate before calling

const inline = process.env.OPENCODE_CONFIG;
if (inline !== undefined) {
  try { JSON.parse(inline.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "")); }
  catch { console.error("OPENCODE_CONFIG is not valid JSONC; unset it or fix it before launching."); process.exit(1); }
}

Type guard

function isJsoncObject(v) {
  try { const p = JSON.parse(v.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, ""));
    return p !== null && typeof p === "object" && !Array.isArray(p); } catch { return false; }
}

Try / catch

try {
  caveman.launch({ agent: "opencode" });
} catch (e) {
  if (e instanceof Error && e.message.includes("cannot preserve opencode inline configuration")) {
    console.error("Fix or unset the OpenCode inline config env var, then relaunch.");
  } else throw e;
}

Prevention

When it happens

Trigger: Launching the opencode agent through the wrapper when process.env[inj.env_var] (OpenCode's inline-config env var) is set to a non-JSONC value: truncated JSON, trailing garbage, an empty string expected to be JSON, or output of a shell command substitution that failed.

Common situations: Exporting OPENCODE config inline with shell quoting mistakes ($(cmd) returning empty); a prior tool writing a partial/invalid config into the variable; mixing YAML/TOML-style content into an env var OpenCode expects to be JSONC.

Understand the failure class

Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.

Related errors


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

Appendix: source

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

      : {};
    let rendered = renderDeep(content, renderedGw, env, renderOptions);
    if (agent.id === "kilo" && mcpMode === "auto") {
      const ownedMcp = ownedMcpRegistration(agent.id, agentArgs);
      if (ownedMcp) {
        // Project exact owned registration into Kilo's highest user-controlled
        // layer. Route preflight rejects active org/managed sources that load
        // afterward, so no later known policy can silently disable recovery.
        rendered = deepMerge(rendered, { mcp: { caveman: kiloMcpEntry(ownedMcp) } });
      }
    }
    if (agent.id === "opencode" && process.env[inj.env_var]) {
      // OpenCode treats inline JSONC as its own configuration layer. Replacing
      // that layer loses the user's model, account, permissions and MCP servers.
      // Preserve native {env:...}/{file:...} references for OpenCode to resolve
      // in the same context; only our routing fields take precedence.
      let original: unknown;
      try { original = parseJsonc(process.env[inj.env_var]!); }
      catch { throw new Error("cannot preserve opencode inline configuration; launching with the original configuration is required"); }
      if (!isPlainObject(original)) throw new Error("opencode inline configuration must be a JSON object");
      rendered = deepMerge(original, rendered);
    }
    env[inj.env_var] = JSON.stringify(rendered);
  } else if (inj.method === "config-file") {
    try {
      applyConfigFileInjection(env, agent, inj, renderedGw, gw, mcpMode, agentArgs, upstreams);
    } catch (e) {
      // OpenClaw ignores the generic base-URL union. Config injection is its only
      // provider redirect, so a failed/missing route must abort the wrapped path;
      // spawnWrapped then launches direct with an explicit warning.
      if (agent.id === "openclaw" || agent.id === "qwen") throw e;
      process.stderr.write(`caveman: ${agent.id} config-file injection failed; using generic env wrap (${(e as Error).message})\n`);
    }
  }
  // Qwen 0.22 discovers MCP servers in the background by default. Its first
  // request can therefore omit caveman_retrieve even though our durable marker
  // says recovery is installed. Proxy compression must never outrun recovery,

View on GitHub (pinned to 3ee70a1026)