JuliusBrussee/caveman · error

MCP config change already running for

Error message

MCP config change already running for ${canonicalPath}

What it means

When the MCP config lock owner is verified live (its PID answers a kill(0) probe) or its state cannot be proven stale (e.g. the owner file is unreadable/malformed and treated as live), a new config change is refused. Stale locks are reclaimed only after a verified-ESRCH probe.

Solutions

  1. Wait for the running caveman MCP command to complete and retry.
  2. Identify the PID in the owner file and terminate it if it is stuck, then retry.
  3. Fix read permissions on the lock/owner files so liveness can be evaluated.
  4. If the lock is genuinely orphaned and no process matches, remove lock and owner files manually, then rerun.

Example fix

// before
caveman mcp add & caveman mcp remove &  # concurrent
// after
caveman mcp add && caveman mcp remove
Defensive patterns

Strategy: retry

Validate before calling

const owner = readOwner(lockPath);
if (owner && isLivePid(owner.pid)) console.error("wait for the running caveman mcp command to finish");

Type guard

const isLivePid = (pid: number) => { try { process.kill(pid, 0); return true; } catch (e) { return (e as NodeJS.ErrnoException).code !== "EPERM"; } };

Try / catch

try { mcpChange(); } catch (e) { if (String(e.message).includes("already running")) { await waitForProcessExit(ownerPid); retry(mcpChange); } }

Prevention

When it happens

Trigger: Starting an MCP config change while another live process holds the lock for the same canonical config path, or the lock state is ambiguous (owner file unreadable) and treated as live.

Common situations: Concurrent caveman MCP commands; a hung but running process holding the lock; permission errors reading the owner claim making staleness undecidable.

Related errors


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

Appendix: source

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

          || Object.keys(existing).sort().join("\0") !== keys.join("\0")
          || existing.schema_version !== 1
          || typeof existing.pid !== "number" || !Number.isInteger(existing.pid) || existing.pid <= 1
          || typeof existing.token !== "string"
          || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(existing.token)
          || existing.config_path !== canonicalPath
          || typeof existing.started_at !== "string"
          || new Date(existing.started_at).toISOString() !== existing.started_at
          || process.platform !== "win32" && ((lockStat.mode | ownerStat.mode) & 0o077) !== 0) {
          throw new Error("invalid MCP lock owner");
        }
        try { process.kill(existing.pid, 0); }
        catch (probeError) { stale = (probeError as NodeJS.ErrnoException).code === "ESRCH"; }
      } catch {
        // Populated claim is durable before publication, so malformed or
        // ownerless lock can never be our crash residue. Never delete it.
        stale = false;
      }
      if (!stale) throw new Error(`MCP config change already running for ${canonicalPath}`);
      const quarantine = `${lock}.stale-${token}`;
      try {
        renameSync(lock, quarantine);
        renameSync(claim, lock);
        fsyncParentDirectory(lock);
        process.stderr.write(`${mark("warn")} reclaimed stale MCP config lock for ${canonicalPath}\n`);
      } catch {
        throw new Error(`MCP config change already running for ${canonicalPath}`);
      } finally {
        try { rmSync(quarantine, { recursive: true, force: true }); } catch { /* isolated stale lock only */ }
      }
    }
    return run();
  } finally {
    try { rmSync(claim, { recursive: true, force: true }); } catch { /* published or absent */ }
    try {
      const current = JSON.parse(readFileSync(join(lock, "owner.json"), "utf8")) as { token?: unknown };
      if (current.token === token) {

View on GitHub (pinned to 3ee70a1026)