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
- 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.
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
- 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
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
- MCP config target changed while acquiring lock; refusing…
- MCP rollback postflight mismatch
- MCP transaction postflight mismatch
- MCP transaction state kept changing; refusing mutation
- changed while planning MCP update; refusing overwrite
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)