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
- Inspect the lock/owner files next to the MCP config; fix or remove them only after confirming no caveman process holds them.
- Restore correct restrictive permissions (no group/world bits, i.e. chmod 600/700) on lock and owner files.
- Re-create the lock cleanly: ensure no caveman process is running, delete the malformed lock, rerun the command.
- 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
- Never hand-edit or copy lock/owner files in the caveman state directory.
- Keep lock files chmod 600/700; avoid restoring them from backups with altered modes.
- Operate on the same canonical config path (avoid symlink aliases).
- Only delete a malformed lock after verifying no caveman process is running.
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
- MCP config target changed while acquiring lock; refusing…
- cursor has unknown key(s)
- cursor must be an object
- must be base64 or null
- filter must be a boolean
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)