{"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":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":8304,"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":8286,"sourceCodeEnd":8322,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L8286-L8322","documentation":"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.","triggerScenarios":"`restoreNativeOperation` for a `claude-settings` operation where `servers.caveman !== undefined` and `JSON.stringify(servers.caveman) !== JSON.stringify(operation.owned.installed_mcp)`.","commonSituations":"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.","solutions":["Restore `mcpServers.caveman` to the exact object recorded in the journal (`owned.installed_mcp`), then retry disable.","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.","Run `caveman doctor claude --fix` to re-sync the MCP entry with the journal, then disable.","Delete the stale journal operation so disable no longer claims ownership of the changed entry."],"exampleFix":"// before (settings.json)\n\"mcpServers\": { \"caveman\": { \"command\": \"caveman\", \"args\": [\"mcp\", \"--verbose\"] } }\n\n// after (match the installed form recorded at enable)\n\"mcpServers\": { \"caveman\": { \"command\": \"caveman\", \"args\": [\"mcp\"] } }","handlingStrategy":"validation","validationCode":"import { readFileSync } from \"node:fs\";\nconst settings = JSON.parse(readFileSync(settingsPath, \"utf8\"));\nconst op = journal.operations.find(o => o.owned?.installed_mcp);\nif (settings.mcpServers?.caveman &&\n    JSON.stringify(settings.mcpServers.caveman) !== JSON.stringify(op.owned.installed_mcp)) {\n  console.error(\"mcpServers.caveman drifted from journal; restore before disable\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Change MCP server config via caveman commands, not by editing agent settings.","Diff `mcpServers.caveman` against the journal after caveman upgrades.","Let Claude Code manage its own MCP entries separately rather than editing the caveman entry."],"tags":["mcp","claude","safety-guard","disable","config-drift"],"backgroundTag":"invalid-state-transition","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}