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
- 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
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
- 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
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
- Claude caveman MCP entry changed after enable; refusing dest
- Gemini caveman MCP entry changed after enable; refusing dest
- OpenCode ${providerID} baseURL changed after enable; refusin
- Hermes ${key.replace("_block", "")} block changed after enab
- Claude caveman-cloud MCP changed after setup; refusing to ov
AI-assisted analysis of JuliusBrussee/caveman@2f49f0e1a3 (2026-08-18).
Data as JSON: /api/errors/fa42eacb985ef99a.
Report an issue: GitHub.