{"record":{"id":"a37e2d43ee55beb2","repo":"JuliusBrussee/caveman","slug":"claude-caveman-mcp-entry-changed-after-enable-ref","errorCode":null,"errorMessage":"Claude caveman MCP entry changed after enable; refusing destructive disable","messagePattern":"Claude caveman MCP entry changed after enable; refusing destructive disable","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":7609,"sourceCode":"    if (beforeEnv.ENABLE_TOOL_SEARCH === undefined && currentEnv.ENABLE_TOOL_SEARCH === TOOL_SEARCH_DEFAULT) {\n      delete currentEnv.ENABLE_TOOL_SEARCH;\n    }\n    if (Object.keys(currentEnv).length > 0) currentRoot.env = currentEnv;\n    else delete currentRoot.env;\n    return jsonBytes(removeNativeHookEntries(currentRoot, \"claude\"));\n  }\n  if (operation.kind === \"claude-mcp\") {\n    const currentRoot = parseJsonFileObject(operation.file, current);\n    const beforeRoot = parseJsonFileObject(operation.file, before);\n    const servers = currentRoot.mcpServers && typeof currentRoot.mcpServers === \"object\" && !Array.isArray(currentRoot.mcpServers)\n      ? currentRoot.mcpServers as Record<string, unknown>\n      : {};\n    const beforeServers = beforeRoot.mcpServers && typeof beforeRoot.mcpServers === \"object\" && !Array.isArray(beforeRoot.mcpServers)\n      ? beforeRoot.mcpServers as Record<string, unknown>\n      : {};\n    const installed = operation.owned?.installed_mcp;\n    if (servers.caveman !== undefined && JSON.stringify(servers.caveman) !== JSON.stringify(installed)) {\n      throw new Error(\"Claude caveman MCP entry changed after enable; refusing destructive disable\");\n    }\n    if (servers.caveman !== undefined) {\n      if (beforeServers.caveman === undefined) delete servers.caveman;\n      else servers.caveman = beforeServers.caveman;\n    }\n    if (Object.keys(servers).length > 0) currentRoot.mcpServers = servers;\n    else delete currentRoot.mcpServers;\n    return jsonBytes(currentRoot);\n  }\n  if (operation.kind === \"codex-hooks\") {\n    return jsonBytes(removeNativeHookEntries(parseJsonFileObject(operation.file, current), \"codex\"));\n  }\n  if (operation.kind === \"gemini-settings\") {\n    const currentRoot = removeNativeHookEntries(parseJsonFileObject(operation.file, current), \"gemini\");\n    const beforeRoot = parseJsonFileObject(operation.file, before);\n    const servers = currentRoot.mcpServers && typeof currentRoot.mcpServers === \"object\" && !Array.isArray(currentRoot.mcpServers)\n      ? currentRoot.mcpServers as Record<string, unknown>\n      : {};","sourceCodeStart":7591,"sourceCodeEnd":7627,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/2f49f0e1a352aa810e70056b7930aeb0b3d219b4/packages/cli/src/index.ts#L7591-L7627","documentation":"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.","triggerScenarios":"`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.","commonSituations":"Wrapping caveman-mcp for logging or env injection; relocating the mcp binary; MCP-managing tools rewriting ~/.claude.json.","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."],"exampleFix":"// before ~/.claude.json\n\"mcpServers\": { \"caveman\": { \"type\": \"stdio\", \"command\": \"./my-wrap\", \"args\": [] } }\n\n// after - restore the journalled entry (or delete the key), then `caveman disable claude`\n\"mcpServers\": { \"caveman\": { \"type\": \"stdio\", \"command\": \"/usr/local/bin/caveman-mcp\", \"args\": [], \"env\": {} } }","handlingStrategy":"validation","validationCode":"import { readFileSync } from 'node:fs';\nconst mcp = JSON.parse(readFileSync(`${process.env.HOME}/.claude.json`, 'utf8'));\nconst journal = JSON.parse(readFileSync(`${process.env.HOME}/.caveman/integrations/claude.json`, 'utf8'));\nconst installed = journal.operations.find((o: any) => o.kind === 'claude-mcp')?.owned?.installed_mcp;\nif (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); }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["claude","mcp","disable","config-drift"],"backgroundTag":"config-drift-refusal","analyzedSha":"2f49f0e1a352aa810e70056b7930aeb0b3d219b4","analyzedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}