JuliusBrussee/caveman · error
Claude caveman MCP entry changed after enable; refusing…
Error message
Claude caveman MCP entry changed after enable; refusing destructive disable
What it means
When disabling the Claude integration, the CLI verifies the `mcpServers.caveman` entry still matches the exact JSON caveman installed (`operation.owned.installed_mcp`). If the entry's serialized JSON differs, someone modified the MCP config after enable, and blindly deleting it could destroy user changes. The CLI therefore refuses the destructive disable.
Solutions
- Restore `mcpServers.caveman` to the exact object recorded in the journal (`owned.installed_mcp`), then retry disable.
- Manually delete the `mcpServers.caveman` key from the Claude settings if you want to keep your modified version, then skip journal restore for that operation.
- Run `caveman doctor claude --fix` to re-sync the MCP entry with the journal, then disable.
- Delete the stale journal operation so disable no longer claims ownership of the changed entry.
Example fix
// before (settings.json)
"mcpServers": { "caveman": { "command": "caveman", "args": ["mcp", "--verbose"] } }
// after (match the installed form recorded at enable)
"mcpServers": { "caveman": { "command": "caveman", "args": ["mcp"] } } Defensive patterns
Strategy: validation
Validate before calling
import { readFileSync } from "node:fs";
const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
const op = journal.operations.find(o => o.owned?.installed_mcp);
if (settings.mcpServers?.caveman &&
JSON.stringify(settings.mcpServers.caveman) !== JSON.stringify(op.owned.installed_mcp)) {
console.error("mcpServers.caveman drifted from journal; restore before disable");
} Prevention
- Change MCP server config via caveman commands, not by editing agent settings.
- Diff `mcpServers.caveman` against the journal after caveman upgrades.
- Let Claude Code manage its own MCP entries separately rather than editing the caveman entry.
When it happens
Trigger: `restoreNativeOperation` for a `claude-settings` operation where `servers.caveman !== undefined` and `JSON.stringify(servers.caveman) !== JSON.stringify(operation.owned.installed_mcp)`.
Common situations: The user edited the caveman MCP server args/env in `~/.claude.json` or settings; Claude Code or another tool rewrote the MCP entry; a caveman upgrade changed the entry shape without journal update.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Gemini caveman MCP entry changed after enable; refusing…
- Caveman hook changed after enable; refusing destructive…
- Claude ANTHROPIC_BASE_URL changed after enable; refusing…
- Gemini routing block changed after enable; refusing…
- caveman-cloud MCP changed after setup; refusing destructive…
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/a37e2d43ee55beb2.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/index.ts:8304
if (beforeEnv.ENABLE_TOOL_SEARCH === undefined && currentEnv.ENABLE_TOOL_SEARCH === TOOL_SEARCH_DEFAULT) {
delete currentEnv.ENABLE_TOOL_SEARCH;
}
if (Object.keys(currentEnv).length > 0) currentRoot.env = currentEnv;
else delete currentRoot.env;
return jsonBytes(removeNativeHookEntries(currentRoot, "claude"));
}
if (operation.kind === "claude-mcp") {
const currentRoot = parseJsonFileObject(operation.file, current);
const beforeRoot = parseJsonFileObject(operation.file, before);
const servers = currentRoot.mcpServers && typeof currentRoot.mcpServers === "object" && !Array.isArray(currentRoot.mcpServers)
? currentRoot.mcpServers as Record<string, unknown>
: {};
const beforeServers = beforeRoot.mcpServers && typeof beforeRoot.mcpServers === "object" && !Array.isArray(beforeRoot.mcpServers)
? beforeRoot.mcpServers as Record<string, unknown>
: {};
const installed = operation.owned?.installed_mcp;
if (servers.caveman !== undefined && JSON.stringify(servers.caveman) !== JSON.stringify(installed)) {
throw new Error("Claude caveman MCP entry changed after enable; refusing destructive disable");
}
if (servers.caveman !== undefined) {
if (beforeServers.caveman === undefined) delete servers.caveman;
else servers.caveman = beforeServers.caveman;
}
if (Object.keys(servers).length > 0) currentRoot.mcpServers = servers;
else delete currentRoot.mcpServers;
return jsonBytes(currentRoot);
}
if (operation.kind === "codex-hooks") {
return jsonBytes(removeNativeHookEntries(parseJsonFileObject(operation.file, current), "codex"));
}
if (operation.kind === "gemini-settings") {
const currentRoot = removeNativeHookEntries(parseJsonFileObject(operation.file, current), "gemini");
const beforeRoot = parseJsonFileObject(operation.file, before);
const servers = currentRoot.mcpServers && typeof currentRoot.mcpServers === "object" && !Array.isArray(currentRoot.mcpServers)
? currentRoot.mcpServers as Record<string, unknown>
: {};View on GitHub (pinned to 3ee70a1026)