JuliusBrussee/caveman · error

Gemini caveman MCP entry changed after enable; refusing…

Error message

Gemini caveman MCP entry changed after enable; refusing destructive disable

What it means

Same ownership check as the Claude case but for Gemini: on disable, the CLI verifies `mcpServers.caveman` in the Gemini settings matches the JSON caveman installed at enable (`operation.owned.installed_mcp`). A divergent entry means the config was modified after enable, so the CLI refuses to destructively remove it.

Solutions

  1. Restore `mcpServers.caveman` to the exact object from the journal's `owned.installed_mcp`, then retry disable.
  2. Manually remove the `mcpServers.caveman` key from the Gemini settings file, then re-run disable.
  3. Run `caveman doctor gemini --fix` to reconcile the entry with the journal, then disable.
  4. Remove the stale journal operation so disable stops asserting ownership over the modified entry.

Example fix

// before (~/.gemini/settings.json)
"mcpServers": { "caveman": { "command": "node", "args": ["/custom/path/caveman-mcp.js"] } }

// after (restore recorded form)
"mcpServers": { "caveman": { "command": "caveman", "args": ["mcp"] } }
Defensive patterns

Strategy: validation

Validate before calling

import { readFileSync } from "node:fs";
const settings = JSON.parse(readFileSync("~/.gemini/settings.json", "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("Gemini mcpServers.caveman drifted from journal; restore before disable");
}

Prevention

When it happens

Trigger: `restoreNativeOperation` for a Gemini settings operation where `servers.caveman !== undefined` and `JSON.stringify(servers.caveman) !== JSON.stringify(operation.owned.installed_mcp)`.

Common situations: Manual edits to the Gemini settings `mcpServers` block; Gemini CLI or another installer rewrote the caveman entry; caveman version upgrade changed the MCP entry definition.

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/89ba1a2bafadf3aa. Report an issue: GitHub.

Appendix: source

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

    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>
      : {};
    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("Gemini 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 === "gemini-env") {
    const block = operation.owned?.route_block;
    if (typeof block !== "string") throw new Error("Gemini integration journal lacks owned routing block");
    const text = current.toString("utf8");
    if (text.includes(GEMINI_NATIVE_ENV_BEGIN) && !text.includes(block)) {
      throw new Error("Gemini routing block changed after enable; refusing destructive disable");
    }
    return Buffer.from(text.replace(`${block}\n\n`, "").replace(`\n\n${block}\n`, "\n").replace(`${block}\n`, "").replace(block, ""));
  }

View on GitHub (pinned to 3ee70a1026)