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

  1. Restore `mcpServers.caveman` to the exact object recorded in the journal (`owned.installed_mcp`), then retry disable.
  2. 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.
  3. Run `caveman doctor claude --fix` to re-sync the MCP entry with the journal, then disable.
  4. 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

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


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)