JuliusBrussee/caveman · warning · Error

changed while planning MCP update; refusing overwrite

Error message

${markerPath} changed while planning MCP update; refusing overwrite

What it means

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.

Solutions

  1. Re-run the command once no other caveman process is running; transient races resolve on retry.
  2. Serialize operations: run 'caveman mcp' commands one at a time rather than in parallel shells.
  3. Identify and stop external writers (watchers, sync daemons) touching the marker directory.
  4. Check the marker wasn't recreated by a competing MCP manager and remove the competition's automation.

Example fix

// before: parallel installs race
(caveman mcp install kilo &) ; caveman mcp uninstall kilo
// after: sequential
sleep 1 && caveman mcp uninstall kilo
Defensive patterns

Strategy: retry

Validate before calling

// no cheap pre-check possible: it is a time-of-check race; just serialize invocations
const lockHint = process.env.CAVEMAN_MCP_BUSY ? 'wait: another caveman mcp command may be running' : null;

Type guard

function markerStableAcrossReads(markerPath: string, delayMs = 50): boolean {
  const a = existsSync(markerPath) ? readFileSync(markerPath) : null;
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delayMs);
  const b = existsSync(markerPath) ? readFileSync(markerPath) : null;
  return (a === null && b === null) || (a !== null && b !== null && a.equals(b));
}

Try / catch

try {
  runMcpCommand();
} catch (e) {
  if (String(e.message).includes('changed while planning MCP update')) {
    await sleep(500);
    runMcpCommand(); // transient race; retry usually succeeds
  }
}

Prevention

When it happens

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

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

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/235b7905447e3403. Report an issue: GitHub.

Appendix: source

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

  mcp?: { command: string; args: string[] },
  lockedConfigPath?: string,
): boolean {
  const plan = action === "install"
    ? agent === "kilo" ? planMcpKiloJson(mcp!, serverName) : planMcpQwenJson(mcp!, serverName)
    : agent === "kilo" ? planRemoveMcpKiloJson(serverName) : planRemoveMcpQwenJson(serverName);
  if (!plan) return false;
  if (lockedConfigPath && plan.path !== canonicalMcpConfigPath(lockedConfigPath)) {
    throw new Error(`${agent} ${serverName} MCP config target changed while acquiring lock; refusing mutation`);
  }

  const markerPath = canonicalOwnedMcpMarkerPath(agent, serverName);
  const markerBefore = fileBytes(markerPath);
  if (markerBefore !== null && !validMcpMarkerBytes(markerBefore, agent, serverName)) {
    throw new Error(`${markerPath} is not a valid Caveman ownership journal; refusing overwrite`);
  }
  const markerBeforeMode = markerBefore === null ? 0o600 : statSync(markerPath).mode & 0o777;
  if (!optionalBytesEqual(fileBytes(markerPath), markerBefore)) {
    throw new Error(`${markerPath} changed while planning MCP update; refusing overwrite`);
  }
  const markerAfter = action === "install" ? mcpMarkerBytes(mcp!, mcpServerToolName(serverName)!, plan.path) : null;
  if (!plan.changed && optionalBytesEqual(markerBefore, markerAfter)) return true;

  const journal: OwnedMcpPendingJournal = {
    schema_version: 1,
    transaction_id: randomUUID(),
    agent,
    server_name: serverName,
    action,
    config_path: plan.path,
    marker_path: markerPath,
    config_before_base64: plan.before?.toString("base64") ?? null,
    config_before_mode: plan.beforeMode,
    config_before_sha256: optionalBytesHash(plan.before),
    config_after_sha256: optionalBytesHash(plan.after),
    marker_before_base64: markerBefore?.toString("base64") ?? null,
    marker_before_mode: markerBeforeMode,

View on GitHub (pinned to 3ee70a1026)