JuliusBrussee/caveman · error · Error

${path} changed during MCP update; refusing overwrite

Error message

${path} changed during MCP update; refusing overwrite

What it means

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.

Source

Thrown at packages/cli/src/index.ts:6819

function durableCreateFile(path: string, bytes: Buffer, mode = 0o600): void {
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
  const temp = join(dirname(path), `.${basename(path)}.caveman-create-${process.pid}-${randomUUID()}.tmp`);
  try {
    durableAtomicWriteFile(temp, bytes, mode);
    linkSync(temp, path);
    fsyncParentDirectory(path);
  } finally {
    try { unlinkSync(temp); } catch { /* published or failed before temp creation */ }
  }
}

function optionalBytesEqual(left: Buffer | null, right: Buffer | null): boolean {
  return left === null ? right === null : right !== null && left.equals(right);
}

function durableReplaceFileIfUnchanged(path: string, expected: Buffer | null, next: Buffer | null, mode = 0o600): void {
  const current = fileBytes(path);
  if (!optionalBytesEqual(current, expected)) throw new Error(`${path} changed during MCP update; refusing overwrite`);
  if (next === null) {
    if (current !== null) {
      const atDelete = fileBytes(path);
      if (!optionalBytesEqual(atDelete, expected)) throw new Error(`${path} changed during MCP update; refusing removal`);
      unlinkSync(path);
      fsyncParentDirectory(path);
    }
    return;
  }

  // Prepare durable replacement first, then run final compare immediately
  // before rename. Per-agent lock serializes Caveman writers; this CAS catches
  // external edits observed before commit without erasing them.
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
  const temp = join(dirname(path), `.${basename(path)}.caveman-cas-${process.pid}-${randomUUID()}.tmp`);
  let fd: number | undefined;
  try {
    fd = openSync(temp, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, mode);

View on GitHub (pinned to 5184b3d11a)

Solutions

  1. Re-run the MCP update so it takes a fresh snapshot of the current file and retries the replacement.
  2. Identify what else modified the file (another CLI run, sync service, editor) and serialize those operations.
  3. Acquire the MCP update lock (see lock owner validation) so only one update runs at a time.
  4. If the external change is unwanted, restore the expected content and retry; if wanted, merge it manually then re-run.

Example fix

// before: concurrent writes clobber each other
writeFileSync(mcpConfigPath, next);

// after: compare-and-swap via durableReplaceFileIfUnchanged with a fresh expected snapshot
const expected = fileBytes(mcpConfigPath);
durableReplaceFileIfUnchanged(mcpConfigPath, expected, next);
Defensive patterns

Strategy: retry

Validate before calling

const expected = fileBytes(path);
if (!optionalBytesEqual(fileBytes(path), expected)) {
  console.error('file changed externally; re-read and retry the update');
}

Type guard

function isUnchanged(path: string, expected: Buffer | null): boolean {
  return optionalBytesEqual(fileBytes(path), expected);
}

Try / catch

try {
  durableReplaceFileIfUnchanged(path, expected, next);
} catch (err) {
  if (String(err.message).includes('changed during MCP update')) {
    // re-read fresh snapshot and retry once, or surface conflict to the user
  } else throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@5184b3d11a (2026-09-06). Data as JSON: /api/errors/ca5abca2c0996dc1. Report an issue: GitHub.