{"record":{"id":"ca5abca2c0996dc1","repo":"JuliusBrussee/caveman","slug":"path-changed-during-mcp-update-refusing-overwr","errorCode":null,"errorMessage":"${path} changed during MCP update; refusing overwrite","messagePattern":"(.+?) changed during MCP update; refusing overwrite","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":6819,"sourceCode":"function durableCreateFile(path: string, bytes: Buffer, mode = 0o600): void {\n  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });\n  const temp = join(dirname(path), `.${basename(path)}.caveman-create-${process.pid}-${randomUUID()}.tmp`);\n  try {\n    durableAtomicWriteFile(temp, bytes, mode);\n    linkSync(temp, path);\n    fsyncParentDirectory(path);\n  } finally {\n    try { unlinkSync(temp); } catch { /* published or failed before temp creation */ }\n  }\n}\n\nfunction optionalBytesEqual(left: Buffer | null, right: Buffer | null): boolean {\n  return left === null ? right === null : right !== null && left.equals(right);\n}\n\nfunction durableReplaceFileIfUnchanged(path: string, expected: Buffer | null, next: Buffer | null, mode = 0o600): void {\n  const current = fileBytes(path);\n  if (!optionalBytesEqual(current, expected)) throw new Error(`${path} changed during MCP update; refusing overwrite`);\n  if (next === null) {\n    if (current !== null) {\n      const atDelete = fileBytes(path);\n      if (!optionalBytesEqual(atDelete, expected)) throw new Error(`${path} changed during MCP update; refusing removal`);\n      unlinkSync(path);\n      fsyncParentDirectory(path);\n    }\n    return;\n  }\n\n  // Prepare durable replacement first, then run final compare immediately\n  // before rename. Per-agent lock serializes Caveman writers; this CAS catches\n  // external edits observed before commit without erasing them.\n  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });\n  const temp = join(dirname(path), `.${basename(path)}.caveman-cas-${process.pid}-${randomUUID()}.tmp`);\n  let fd: number | undefined;\n  try {\n    fd = openSync(temp, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, mode);","sourceCodeStart":6801,"sourceCodeEnd":6837,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/5184b3d11ac6a1acb7d44b9bfaa31698157cff97/packages/cli/src/index.ts#L6801-L6837","documentation":"durableReplaceFileIfUnchanged is a compare-and-swap style durable file replacement used during MCP updates. Before overwriting the target it re-reads the file and verifies its bytes still match the expected snapshot taken earlier. If another process (or user) modified the file between the snapshot and the write, the update aborts rather than clobbering concurrent changes.","triggerScenarios":"Running an MCP update that calls durableReplaceFileIfUnchanged while the target file at path was modified after the expected snapshot was captured — e.g. a concurrent CLI process, an editor auto-save, or another tool rewrote the config between read and replace.","commonSituations":"Two CLI instances updating MCP config simultaneously; a sync tool (Dropbox/iCloud) touching the config file mid-update; an editor with unsaved-buffer auto-write racing the update.","solutions":["Re-run the MCP update so it takes a fresh snapshot of the current file and retries the replacement.","Identify what else modified the file (another CLI run, sync service, editor) and serialize those operations.","Acquire the MCP update lock (see lock owner validation) so only one update runs at a time.","If the external change is unwanted, restore the expected content and retry; if wanted, merge it manually then re-run."],"exampleFix":"// before: concurrent writes clobber each other\nwriteFileSync(mcpConfigPath, next);\n\n// after: compare-and-swap via durableReplaceFileIfUnchanged with a fresh expected snapshot\nconst expected = fileBytes(mcpConfigPath);\ndurableReplaceFileIfUnchanged(mcpConfigPath, expected, next);","handlingStrategy":"retry","validationCode":"const expected = fileBytes(path);\nif (!optionalBytesEqual(fileBytes(path), expected)) {\n  console.error('file changed externally; re-read and retry the update');\n}","typeGuard":"function isUnchanged(path: string, expected: Buffer | null): boolean {\n  return optionalBytesEqual(fileBytes(path), expected);\n}","tryCatchPattern":"try {\n  durableReplaceFileIfUnchanged(path, expected, next);\n} catch (err) {\n  if (String(err.message).includes('changed during MCP update')) {\n    // re-read fresh snapshot and retry once, or surface conflict to the user\n  } else throw err;\n}","preventionTips":["Run MCP updates under the update lock so only one writer exists.","Exclude config directories from file-sync tools (Dropbox/iCloud/OneDrive).","Close editors with open buffers on the config file before updating.","Retry updates automatically on this error with a fresh snapshot."],"tags":["mcp","concurrency","file-write","atomic-update"],"backgroundTag":"checksum-mismatch","analyzedSha":"5184b3d11ac6a1acb7d44b9bfaa31698157cff97","analyzedAt":"2026-09-06T12:00:26.372Z","contentChangedAt":"2026-09-06T12:00:26.372Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}