JuliusBrussee/caveman · error · Error
MCP config target changed while acquiring lock; refusing…
Error message
${agent} ${serverName} MCP config target changed while acquiring lock; refusing mutation What it means
transactOwnedMcpConfig re-plans the MCP change inside the lock and checks that the resolved config file path still matches the path that was locked (lockedConfigPath). If planning now resolves to a different config file than the one the lock was acquired for, the CLI refuses to mutate, because writing would not be protected by the held lock.
Solutions
- Re-run the command; if the path resolution is now stable the transaction will proceed.
- Check for and stop processes that mutate HOME/XDG_CONFIG_HOME or move/symlink agent config directories.
- Verify the agent config location is stable: remove dangling symlinks or duplicate configs so plan resolution is deterministic.
- Ensure only one caveman process operates on MCP config at a time (the lock exists for this — don't bypass it).
Example fix
// before: HOME flips between lock and plan export XDG_CONFIG_HOME=/tmp/new; caveman mcp install kilo // after: run in a stable environment unset XDG_CONFIG_HOME && caveman mcp install kilo
Defensive patterns
Strategy: retry
Validate before calling
const before = resolveMcpConfigPath(agent);
const after = resolveMcpConfigPath(agent); // re-resolve
if (before !== after) console.error('config path resolution is unstable; fix env/symlinks before running caveman mcp'); Type guard
function pathResolutionIsStable(resolve: () => string): boolean {
return resolve() === resolve();
} Try / catch
try {
runMcpCommand();
} catch (e) {
if (String(e.message).includes('target changed while acquiring lock')) {
// retry in a stable environment
runMcpCommand();
}
} Prevention
- Keep HOME and XDG_CONFIG_HOME constant for the life of the command
- Don't move/symlink agent config directories while caveman commands run
- Avoid scripts that mutate env vars globally (e.g. direnv reloads) mid-command
- Run one caveman mcp operation at a time so lock waiters see stable paths
When it happens
Trigger: Thrown when lockedConfigPath is set and plan.path !== canonicalMcpConfigPath(lockedConfigPath) — the plan (planMcpKiloJson/planMcpQwenJson or the remove variants) resolves to a different config path between lock acquisition and execution, e.g. because HOME, XDG env vars, or agent config discovery changed mid-run.
Common situations: Concurrent process changed an env var the config resolution depends on; the agent's config was created/moved at a new location while the command was waiting for the lock; symlink retargeting of a config directory during the wait; test harnesses overriding HOME mid-flight.
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 rollback postflight mismatch
- MCP transaction postflight mismatch
- invalid MCP lock owner
- changed while planning MCP update; refusing overwrite
- MCP config change already running for
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/e4742aaea618b300.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/index.ts:12880
}
removePendingCopies();
process.stderr.write(`${mark("warn")} rolled back interrupted ${agent} ${serverName} MCP transaction\n`);
return "rolled-back";
}
function transactOwnedMcpConfig(
agent: "kilo" | "qwen",
serverName: string,
action: "install" | "uninstall",
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,View on GitHub (pinned to 3ee70a1026)