{"record":{"id":"fa42eacb985ef99a","repo":"JuliusBrussee/caveman","slug":"opencode-caveman-mcp-entry-changed-after-enable-r","errorCode":null,"errorMessage":"OpenCode caveman MCP entry changed after enable; refusing destructive disable","messagePattern":"OpenCode caveman MCP entry changed after enable; refusing destructive disable","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":7677,"sourceCode":"    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> : {};\n    for (const providerID of [\"openai\", \"anthropic\"]) {\n      const provider = providers[providerID] && typeof providers[providerID] === \"object\" && !Array.isArray(providers[providerID]) ? providers[providerID] as Record<string, unknown> : {};\n      const options = provider.options && typeof provider.options === \"object\" && !Array.isArray(provider.options) ? provider.options as Record<string, unknown> : {};\n      if (options.baseURL !== undefined && options.baseURL !== routes[providerID]) {\n        throw new Error(`OpenCode ${providerID} baseURL changed after enable; refusing destructive disable`);\n      }\n      if (previousRoutes[providerID] === null || previousRoutes[providerID] === undefined) delete options.baseURL;\n      else options.baseURL = previousRoutes[providerID];\n      if (Object.keys(options).length > 0) provider.options = options;\n      else delete provider.options;\n      if (Object.keys(provider).length > 0) providers[providerID] = provider;\n      else delete providers[providerID];\n    }\n    if (Object.keys(providers).length > 0) root.provider = providers;\n    else delete root.provider;\n    const mcp = root.mcp && typeof root.mcp === \"object\" && !Array.isArray(root.mcp) ? root.mcp as Record<string, unknown> : {};\n    if (mcp.caveman !== undefined && JSON.stringify(mcp.caveman) !== JSON.stringify(operation.owned?.installed_mcp)) {\n      throw new Error(\"OpenCode caveman MCP entry changed after enable; refusing destructive disable\");\n    }\n    if (mcp.caveman !== undefined) {\n      if (operation.owned?.previous_mcp === null || operation.owned?.previous_mcp === undefined) delete mcp.caveman;\n      else mcp.caveman = operation.owned.previous_mcp;\n    }\n    if (Object.keys(mcp).length > 0) root.mcp = mcp;\n    else delete root.mcp;\n    return jsonBytes(root);\n  }\n  if (operation.kind === \"aider-config\") {\n    let text = current.toString(\"utf8\");\n    const routeBlock = operation.owned?.route_block;\n    const readBlock = operation.owned?.read_block;\n    const previousRouteLine = operation.owned?.previous_route_line;\n    if (typeof routeBlock !== \"string\" || typeof readBlock !== \"string\") {\n      throw new Error(\"Aider integration journal lacks owned blocks\");\n    }\n    if (!text.includes(routeBlock) || !text.includes(readBlock)) {","sourceCodeStart":7659,"sourceCodeEnd":7695,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/2f49f0e1a352aa810e70056b7930aeb0b3d219b4/packages/cli/src/index.ts#L7659-L7695","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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`","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","Re-run `caveman enable opencode` so the journal records the current entry, then disable"],"exampleFix":"// before: mcp.caveman was edited after enable (args changed)\n\"mcp\": { \"caveman\": { \"command\": \"caveman-mcp\", \"args\": [\"--verbose\"] } }\n\n// after: entry byte-equivalent to owned.installed_mcp, disable succeeds\n\"mcp\": { \"caveman\": { \"command\": \"caveman-mcp\", \"args\": [] } }","handlingStrategy":"validation","validationCode":"// Structural equality check mirroring the CLI's guard\nimport { readFileSync } from 'node:fs';\nconst home = process.env.CAVEMAN_HOME ?? `${process.env.HOME}/.caveman`;\nconst journal = JSON.parse(readFileSync(`${home}/integrations/opencode.json`, 'utf8'));\nconst cfg = JSON.parse(readFileSync(process.env.OPENCODE_CONFIG ?? `${process.env.HOME}/.config/opencode/opencode.json`, 'utf8'));\nfor (const op of journal.operations) {\n  if (cfg.mcp?.caveman !== undefined && JSON.stringify(cfg.mcp.caveman) !== JSON.stringify(op.owned?.installed_mcp)) {\n    throw new Error('refusing: mcp.caveman drifted');\n  }\n}","typeGuard":"const isUnmodifiedMcpEntry = (live, journaled) =>\n  live === undefined || JSON.stringify(live) === JSON.stringify(journaled);","tryCatchPattern":"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; }","preventionTips":["Leave the mcp.caveman entry untouched; customize other MCP servers instead","If you need a different caveman-mcp binary, re-run `caveman enable opencode` afterwards so the journal matches"],"tags":["opencode","mcp","config-drift","json-config","disable"],"backgroundTag":"config-drift-detected","analyzedSha":"2f49f0e1a352aa810e70056b7930aeb0b3d219b4","analyzedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}