JuliusBrussee/caveman · error

invalid MCP lock owner

Error message

invalid MCP lock owner

What it means

The MCP config lock carries a JSON claim describing its owner: pid, uuid-v4 token, canonical config path, ISO timestamp, and file mode restrictions. If the claim exists but fails any structural or semantic check (bad token format, mismatched config path, non-ISO timestamp, group/world-writable modes), the lock is considered corrupt — not crash residue, since the claim is written durably before publication — so the tool throws instead of touching it.

Solutions

  1. Inspect the lock/owner files next to the MCP config; fix or remove them only after confirming no caveman process holds them.
  2. Restore correct restrictive permissions (no group/world bits, i.e. chmod 600/700) on lock and owner files.
  3. Re-create the lock cleanly: ensure no caveman process is running, delete the malformed lock, rerun the command.
  4. Ensure you operate on the same canonical config path the lock was created for (avoid symlinks/path aliasing).

Example fix

// before
ls -l ~/.caveman/mcp.lock  # -rw-rw-rw- (world-writable)
// after
chmod 600 ~/.caveman/mcp.lock && caveman mcp sync
Defensive patterns

Strategy: validation

Validate before calling

import { statSync, readFileSync } from "node:fs";
const st = statSync(lockPath);
if ((st.mode & 0o077) !== 0) console.error("lock file too permissive; chmod 600 before running");
const owner = JSON.parse(readFileSync(lockPath, "utf8"));
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(owner.token ?? "")) console.error("malformed lock claim");

Type guard

const isValidClaim = (o: unknown): o is { pid: number; token: string; config_path: string; started_at: string } =>
  typeof o === "object" && o !== null && "token" in o && /^[0-9a-f-]{36}$/.test((o as any).token);

Try / catch

try { mcpSync(); } catch (e) { if (String(e.message).includes("invalid MCP lock owner")) { inspectAndRebuildLock(lockPath); } }

Prevention

When it happens

Trigger: Running an MCP config change while the lock file exists with a populated but malformed/unverifiable owner claim: tampered or truncated JSON, token failing the UUID-v4 regex, config_path differing from the canonical path, or overly permissive file modes.

Common situations: Manual editing or partial writes of the lock file; a different tool copying the lock; files restored from backups with changed permissions; running across hosts sharing a directory so paths/modes differ.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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

Appendix: source

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

      try {
        const lockStat = lstatSync(lock);
        const ownerPath = join(lock, "owner.json");
        const ownerStat = lstatSync(ownerPath);
        const existing = JSON.parse(readFileSync(ownerPath, "utf8")) as Record<string, unknown>;
        const keys = ["config_path", "pid", "schema_version", "started_at", "token"];
        if (!lockStat.isDirectory() || lockStat.isSymbolicLink()
          || !ownerStat.isFile() || ownerStat.isSymbolicLink()
          || readdirSync(lock).sort().join("\0") !== "owner.json"
          || 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 {

View on GitHub (pinned to 3ee70a1026)