{"record":{"id":"fd1f5f12140ed824","repo":"JuliusBrussee/caveman","slug":"path-changed-during-mcp-update-refusing-remova","errorCode":null,"errorMessage":"${path} changed during MCP update; refusing removal","messagePattern":"(.+?) changed during MCP update; refusing removal","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":6823,"sourceCode":"    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);\n    writeFileSync(fd, next);\n    fchmodSync(fd, mode);\n    fsyncSync(fd);\n    closeSync(fd);","sourceCodeStart":6805,"sourceCodeEnd":6841,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/5184b3d11ac6a1acb7d44b9bfaa31698157cff97/packages/cli/src/index.ts#L6805-L6841","documentation":"When an MCP update determines a file should be removed (next === null), it double-checks the file's bytes immediately before unlinking. If the bytes no longer match the expected snapshot, the file was modified concurrently and the removal is refused to avoid deleting someone else's new content.","triggerScenarios":"durableReplaceFileIfUnchanged called with next=null (removal path) while the file at path was written between the initial fileBytes read and the pre-delete re-read — concurrent update, editor save, or sync-tool write.","commonSituations":"Removing an obsolete MCP entry while another process has just recreated/edited that same config file; file-sync software resurrecting or modifying the file during cleanup.","solutions":["Re-run the MCP update so a fresh snapshot is taken and the removal decision is re-evaluated against current content.","Check whether another process or sync service modified the file and coordinate/serialize updates.","If the file legitimately changed and should be kept, skip the removal manually or adjust update inputs.","Run the update under the MCP lock to exclude concurrent writers."],"exampleFix":"// before: blind delete can drop concurrent edits\nunlinkSync(configPath);\n\n// after: verify before delete\nconst current = fileBytes(configPath);\nif (optionalBytesEqual(current, expected)) unlinkSync(configPath);","handlingStrategy":"retry","validationCode":"if (fileBytes(path) !== null && !optionalBytesEqual(fileBytes(path), expected)) {\n  console.error('target modified externally; skip removal and re-evaluate');\n}","typeGuard":"function isSafeToRemove(path: string, expected: Buffer): boolean {\n  return optionalBytesEqual(fileBytes(path), expected);\n}","tryCatchPattern":"try {\n  durableReplaceFileIfUnchanged(path, expected, null);\n} catch (err) {\n  if (String(err.message).includes('refusing removal')) {\n    // re-run update so removal is re-decided against current content\n  } else throw err;\n}","preventionTips":["Serialize removals and updates through the MCP lock.","Do not hand-edit files the updater manages while an update runs.","Disable sync tools for the config directory during maintenance.","Re-run the update after any concurrent-modification error."],"tags":["mcp","concurrency","file-deletion","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"}