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
- 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.
- Or delete the `caveman` key from mcpServers yourself, then re-run disable.
- 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
- Never rename or rewrap the `caveman` MCP entry - register custom servers under other names
- If you must change it, run `caveman doctor claude --fix` afterwards to re-canonicalize
- Keep MCP-config tools from rewriting ~/.claude.json key order
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
- Claude caveman-cloud MCP changed after setup; refusing to ov
- ${agent} ${event} Caveman hook changed after enable; refusin
- Claude ANTHROPIC_BASE_URL changed after enable; refusing des
- Gemini caveman MCP entry changed after enable; refusing dest
- OpenCode caveman MCP entry changed after enable; refusing de
AI-assisted analysis of JuliusBrussee/caveman@2f49f0e1a3 (2026-08-18).
Data as JSON: /api/errors/a37e2d43ee55beb2.
Report an issue: GitHub.