JuliusBrussee/caveman · error · Error

Claude caveman MCP entry changed after enable; refusing dest

Error message

Claude caveman MCP entry changed after enable; refusing destructive disable

What it means

For the claude-mcp operation, disable only removes mcpServers.caveman from ~/.claude.json when it still JSON-matches the entry recorded at enable time (owned.installed_mcp: {type:'stdio', command:<mcpBinary>, args:[], env:{}}). A changed entry - different command, args, or env - is treated as user-owned content now; deleting it blindly could lose your edits, so disable aborts.

Source

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

    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 2f49f0e1a3)

Solutions

  1. Revert mcpServers.caveman to the journalled value: cat ~/.caveman/integrations/claude.json and copy owned.installed_mcp from the claude-mcp operation, then re-run disable.
  2. Or delete the `caveman` key from mcpServers yourself, then re-run disable.
  3. Register custom MCP servers under a name other than `caveman` so they never collide with managed entries.

Example fix

// before ~/.claude.json
"mcpServers": { "caveman": { "type": "stdio", "command": "./my-wrap", "args": [] } }

// after - restore the journalled entry (or delete the key), then `caveman disable claude`
"mcpServers": { "caveman": { "type": "stdio", "command": "/usr/local/bin/caveman-mcp", "args": [], "env": {} } }
Defensive patterns

Strategy: validation

Validate before calling

import { readFileSync } from 'node:fs';
const mcp = JSON.parse(readFileSync(`${process.env.HOME}/.claude.json`, 'utf8'));
const journal = JSON.parse(readFileSync(`${process.env.HOME}/.caveman/integrations/claude.json`, 'utf8'));
const installed = journal.operations.find((o: any) => o.kind === 'claude-mcp')?.owned?.installed_mcp;
if (mcp?.mcpServers?.caveman !== undefined && JSON.stringify(mcp.mcpServers.caveman) !== JSON.stringify(installed)) { console.error('mcpServers.caveman drifted from journal; revert or remove it before disable'); process.exit(1); }

Prevention

When it happens

Trigger: `caveman disable claude` after editing the `caveman` entry in the mcpServers object of ~/.claude.json - e.g. changing command to a wrapper, adding args, or a tool reformatting the object so it no longer stringifies identically.

Common situations: Wrapping caveman-mcp for logging or env injection; relocating the mcp binary; MCP-managing tools rewriting ~/.claude.json.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@2f49f0e1a3 (2026-08-18). Data as JSON: /api/errors/a37e2d43ee55beb2. Report an issue: GitHub.