{"record":{"id":"e4742aaea618b300","repo":"JuliusBrussee/caveman","slug":"agent-servername-mcp-config-target-changed-w","errorCode":null,"errorMessage":"${agent} ${serverName} MCP config target changed while acquiring lock; refusing mutation","messagePattern":"(.+?) (.+?) MCP config target changed while acquiring lock; refusing mutation","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":12880,"sourceCode":"  }\n  removePendingCopies();\n  process.stderr.write(`${mark(\"warn\")} rolled back interrupted ${agent} ${serverName} MCP transaction\\n`);\n  return \"rolled-back\";\n}\n\nfunction transactOwnedMcpConfig(\n  agent: \"kilo\" | \"qwen\",\n  serverName: string,\n  action: \"install\" | \"uninstall\",\n  mcp?: { command: string; args: string[] },\n  lockedConfigPath?: string,\n): boolean {\n  const plan = action === \"install\"\n    ? agent === \"kilo\" ? planMcpKiloJson(mcp!, serverName) : planMcpQwenJson(mcp!, serverName)\n    : agent === \"kilo\" ? planRemoveMcpKiloJson(serverName) : planRemoveMcpQwenJson(serverName);\n  if (!plan) return false;\n  if (lockedConfigPath && plan.path !== canonicalMcpConfigPath(lockedConfigPath)) {\n    throw new Error(`${agent} ${serverName} MCP config target changed while acquiring lock; refusing mutation`);\n  }\n\n  const markerPath = canonicalOwnedMcpMarkerPath(agent, serverName);\n  const markerBefore = fileBytes(markerPath);\n  if (markerBefore !== null && !validMcpMarkerBytes(markerBefore, agent, serverName)) {\n    throw new Error(`${markerPath} is not a valid Caveman ownership journal; refusing overwrite`);\n  }\n  const markerBeforeMode = markerBefore === null ? 0o600 : statSync(markerPath).mode & 0o777;\n  if (!optionalBytesEqual(fileBytes(markerPath), markerBefore)) {\n    throw new Error(`${markerPath} changed while planning MCP update; refusing overwrite`);\n  }\n  const markerAfter = action === \"install\" ? mcpMarkerBytes(mcp!, mcpServerToolName(serverName)!, plan.path) : null;\n  if (!plan.changed && optionalBytesEqual(markerBefore, markerAfter)) return true;\n\n  const journal: OwnedMcpPendingJournal = {\n    schema_version: 1,\n    transaction_id: randomUUID(),\n    agent,","sourceCodeStart":12862,"sourceCodeEnd":12898,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L12862-L12898","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before: HOME flips between lock and plan\nexport XDG_CONFIG_HOME=/tmp/new; caveman mcp install kilo\n// after: run in a stable environment\nunset XDG_CONFIG_HOME && caveman mcp install kilo","handlingStrategy":"retry","validationCode":"const before = resolveMcpConfigPath(agent);\nconst after = resolveMcpConfigPath(agent); // re-resolve\nif (before !== after) console.error('config path resolution is unstable; fix env/symlinks before running caveman mcp');","typeGuard":"function pathResolutionIsStable(resolve: () => string): boolean {\n  return resolve() === resolve();\n}","tryCatchPattern":"try {\n  runMcpCommand();\n} catch (e) {\n  if (String(e.message).includes('target changed while acquiring lock')) {\n    // retry in a stable environment\n    runMcpCommand();\n  }\n}","preventionTips":["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"],"tags":["mcp","locking","race-condition","path-resolution"],"backgroundTag":"invalid-state-transition","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}