JuliusBrussee/caveman · error · Error

OpenCode caveman MCP entry changed after enable; refusing de

Error message

OpenCode caveman MCP entry changed after enable; refusing destructive disable

What it means

Thrown during `caveman disable opencode` / `caveman doctor opencode --fix` when the mcp.caveman entry in opencode.json exists but its JSON serialization no longer equals owned.installed_mcp from the integration journal. Caveman compares the entry structurally (JSON.stringify equality), so any edit to the server command, args, or env of its own MCP registration makes the disable refuse rather than overwrite your modified entry with the pre-enable one.

Source

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

    const previousRoutes = operation.owned?.previous_routes && typeof operation.owned.previous_routes === "object" && !Array.isArray(operation.owned.previous_routes) ? operation.owned.previous_routes as Record<string, unknown> : {};
    for (const providerID of ["openai", "anthropic"]) {
      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> : {};
      if (options.baseURL !== undefined && options.baseURL !== routes[providerID]) {
        throw new Error(`OpenCode ${providerID} baseURL changed after enable; refusing destructive disable`);
      }
      if (previousRoutes[providerID] === null || previousRoutes[providerID] === undefined) delete options.baseURL;
      else options.baseURL = previousRoutes[providerID];
      if (Object.keys(options).length > 0) provider.options = options;
      else delete provider.options;
      if (Object.keys(provider).length > 0) providers[providerID] = provider;
      else delete providers[providerID];
    }
    if (Object.keys(providers).length > 0) root.provider = providers;
    else delete root.provider;
    const mcp = root.mcp && typeof root.mcp === "object" && !Array.isArray(root.mcp) ? root.mcp as Record<string, unknown> : {};
    if (mcp.caveman !== undefined && JSON.stringify(mcp.caveman) !== JSON.stringify(operation.owned?.installed_mcp)) {
      throw new Error("OpenCode caveman MCP entry changed after enable; refusing destructive disable");
    }
    if (mcp.caveman !== undefined) {
      if (operation.owned?.previous_mcp === null || operation.owned?.previous_mcp === undefined) delete mcp.caveman;
      else mcp.caveman = operation.owned.previous_mcp;
    }
    if (Object.keys(mcp).length > 0) root.mcp = mcp;
    else delete root.mcp;
    return jsonBytes(root);
  }
  if (operation.kind === "aider-config") {
    let text = current.toString("utf8");
    const routeBlock = operation.owned?.route_block;
    const readBlock = operation.owned?.read_block;
    const previousRouteLine = operation.owned?.previous_route_line;
    if (typeof routeBlock !== "string" || typeof readBlock !== "string") {
      throw new Error("Aider integration journal lacks owned blocks");
    }
    if (!text.includes(routeBlock) || !text.includes(readBlock)) {

View on GitHub (pinned to 2f49f0e1a3)

Solutions

  1. Restore mcp.caveman to the exact object recorded in owned.installed_mcp of ~/.caveman/integrations/opencode.json (key order does not matter, contents do), then rerun `caveman disable opencode`
  2. If the modified entry is what you want: copy it aside, disable will not run — instead manually delete the mcp.caveman key and the journal file
  3. Re-run `caveman enable opencode` so the journal records the current entry, then disable

Example fix

// before: mcp.caveman was edited after enable (args changed)
"mcp": { "caveman": { "command": "caveman-mcp", "args": ["--verbose"] } }

// after: entry byte-equivalent to owned.installed_mcp, disable succeeds
"mcp": { "caveman": { "command": "caveman-mcp", "args": [] } }
Defensive patterns

Strategy: validation

Validate before calling

// Structural equality check mirroring the CLI's guard
import { readFileSync } from 'node:fs';
const home = process.env.CAVEMAN_HOME ?? `${process.env.HOME}/.caveman`;
const journal = JSON.parse(readFileSync(`${home}/integrations/opencode.json`, 'utf8'));
const cfg = JSON.parse(readFileSync(process.env.OPENCODE_CONFIG ?? `${process.env.HOME}/.config/opencode/opencode.json`, 'utf8'));
for (const op of journal.operations) {
  if (cfg.mcp?.caveman !== undefined && JSON.stringify(cfg.mcp.caveman) !== JSON.stringify(op.owned?.installed_mcp)) {
    throw new Error('refusing: mcp.caveman drifted');
  }
}

Type guard

const isUnmodifiedMcpEntry = (live, journaled) =>
  live === undefined || JSON.stringify(live) === JSON.stringify(journaled);

Try / catch

catch (err) { if (err.message.includes('MCP entry changed after enable')) { /* restore mcp.caveman from journal or delete the key, then retry */ } else throw err; }

Prevention

When it happens

Trigger: Editing the mcp.caveman object in opencode.json after `caveman enable opencode` — e.g. changing the command path, adding args, or altering env — and then running `caveman disable opencode` while the file hash also differs from after_sha256.

Common situations: Pointing the caveman MCP server at a different binary (e.g. a locally built caveman-mcp); adding environment variables to the MCP entry; a newer opencode version normalizing/rewriting its config file including the caveman entry.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@2f49f0e1a3 (2026-08-18). Data as JSON: /api/errors/fa42eacb985ef99a. Report an issue: GitHub.