{"record":{"id":"235b7905447e3403","repo":"JuliusBrussee/caveman","slug":"markerpath-changed-while-planning-mcp-update-r","errorCode":null,"errorMessage":"${markerPath} changed while planning MCP update; refusing overwrite","messagePattern":"(.+?) changed while planning MCP update; refusing overwrite","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"packages/cli/src/index.ts","lineNumber":12890,"sourceCode":"  mcp?: { command: string; args: string[] },\n  lockedConfigPath?: string,\n): boolean {\n  const plan = action === \"install\"\n    ? agent === \"kilo\" ? planMcpKiloJson(mcp!, serverName) : planMcpQwenJson(mcp!, serverName)\n    : agent === \"kilo\" ? planRemoveMcpKiloJson(serverName) : planRemoveMcpQwenJson(serverName);\n  if (!plan) return false;\n  if (lockedConfigPath && plan.path !== canonicalMcpConfigPath(lockedConfigPath)) {\n    throw new Error(`${agent} ${serverName} MCP config target changed while acquiring lock; refusing mutation`);\n  }\n\n  const markerPath = canonicalOwnedMcpMarkerPath(agent, serverName);\n  const markerBefore = fileBytes(markerPath);\n  if (markerBefore !== null && !validMcpMarkerBytes(markerBefore, agent, serverName)) {\n    throw new Error(`${markerPath} is not a valid Caveman ownership journal; refusing overwrite`);\n  }\n  const markerBeforeMode = markerBefore === null ? 0o600 : statSync(markerPath).mode & 0o777;\n  if (!optionalBytesEqual(fileBytes(markerPath), markerBefore)) {\n    throw new Error(`${markerPath} changed while planning MCP update; refusing overwrite`);\n  }\n  const markerAfter = action === \"install\" ? mcpMarkerBytes(mcp!, mcpServerToolName(serverName)!, plan.path) : null;\n  if (!plan.changed && optionalBytesEqual(markerBefore, markerAfter)) return true;\n\n  const journal: OwnedMcpPendingJournal = {\n    schema_version: 1,\n    transaction_id: randomUUID(),\n    agent,\n    server_name: serverName,\n    action,\n    config_path: plan.path,\n    marker_path: markerPath,\n    config_before_base64: plan.before?.toString(\"base64\") ?? null,\n    config_before_mode: plan.beforeMode,\n    config_before_sha256: optionalBytesHash(plan.before),\n    config_after_sha256: optionalBytesHash(plan.after),\n    marker_before_base64: markerBefore?.toString(\"base64\") ?? null,\n    marker_before_mode: markerBeforeMode,","sourceCodeStart":12872,"sourceCodeEnd":12908,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L12872-L12908","documentation":"The CLI reads the ownership marker, records its bytes, and then immediately re-reads the file to confirm nothing changed between the two reads (a TOCTOU guard). If the re-read differs from the first snapshot, another process modified the marker while this transaction was planning, and the CLI aborts instead of overwriting based on stale state.","triggerScenarios":"Thrown in transactOwnedMcpConfig when !optionalBytesEqual(fileBytes(markerPath), markerBefore) — the marker file changed between the initial read and the verification re-read, e.g. a concurrent 'caveman mcp' command or another writer touched the marker.","commonSituations":"Two caveman commands (e.g. install and uninstall) racing without the lock; a file watcher/backup tool touching the marker; the agent process rewriting markers concurrently; NFS/network filesystem timestamp quirks.","solutions":["Re-run the command once no other caveman process is running; transient races resolve on retry.","Serialize operations: run 'caveman mcp' commands one at a time rather than in parallel shells.","Identify and stop external writers (watchers, sync daemons) touching the marker directory.","Check the marker wasn't recreated by a competing MCP manager and remove the competition's automation."],"exampleFix":"// before: parallel installs race\n(caveman mcp install kilo &) ; caveman mcp uninstall kilo\n// after: sequential\nsleep 1 && caveman mcp uninstall kilo","handlingStrategy":"retry","validationCode":"// no cheap pre-check possible: it is a time-of-check race; just serialize invocations\nconst lockHint = process.env.CAVEMAN_MCP_BUSY ? 'wait: another caveman mcp command may be running' : null;","typeGuard":"function markerStableAcrossReads(markerPath: string, delayMs = 50): boolean {\n  const a = existsSync(markerPath) ? readFileSync(markerPath) : null;\n  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delayMs);\n  const b = existsSync(markerPath) ? readFileSync(markerPath) : null;\n  return (a === null && b === null) || (a !== null && b !== null && a.equals(b));\n}","tryCatchPattern":"try {\n  runMcpCommand();\n} catch (e) {\n  if (String(e.message).includes('changed while planning MCP update')) {\n    await sleep(500);\n    runMcpCommand(); // transient race; retry usually succeeds\n  }\n}","preventionTips":["Never run multiple caveman mcp commands concurrently — serialize them","Stop watcher/backup tools from touching ~/.caveman marker files","Prefer retries over manual marker edits when this error appears","Check for cron jobs or MCP managers that periodically rewrite markers"],"tags":["mcp","marker","race-condition","concurrency"],"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"}