JuliusBrussee/caveman · error · Error
invalid MCP lock owner
Error message
invalid MCP lock owner
What it means
Before reusing or probing an existing MCP update lock, the code validates the lock owner record: the token must be a UUIDv4 string, config_path must match the canonical path, started_at must be a valid ISO timestamp, and on non-Windows platforms neither the lock file nor the owner file may be group/other accessible. Any violation means the lock is not a legitimate owner record, so it throws instead of trusting or deleting it.
Source
Thrown at packages/cli/src/index.ts:7783
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 5184b3d11a)
Solutions
- Verify no MCP update is actually running, then remove the malformed lock file and retry the update.
- Check the lock file mode on POSIX (chmod 600 lock and owner files) if permissions are the violation.
- Ensure you are operating on the same canonical config path the lock was created for.
- Upgrade/reinstall the CLI if a stale lock format from an older version is the cause.
Example fix
// before: blindly trusting a hand-edited lock
const existing = JSON.parse(readFileSync(lockPath));
probe(existing.pid);
// after: validate owner record first
if (!isUuidV4(existing.token) || existing.config_path !== canonicalPath) {
rmSync(lockPath); // only after confirming no live owner
} Defensive patterns
Strategy: validation
Validate before calling
function isValidLockOwner(existing) {
const uuidV4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
return existing
&& typeof existing.token === 'string' && uuidV4.test(existing.token)
&& existing.config_path === canonicalPath
&& typeof existing.started_at === 'string'
&& new Date(existing.started_at).toISOString() === existing.started_at;
}
// call before attempting to reuse or probe the lock Type guard
function isWellFormedLock(v: unknown): v is { token: string; config_path: string; started_at: string; pid: number } {
return typeof v === 'object' && v !== null
&& typeof (v as any).token === 'string'
&& typeof (v as any).config_path === 'string'
&& typeof (v as any).started_at === 'string'
&& typeof (v as any).pid === 'number';
} Try / catch
try {
acquireOrReuseMcpLock(lockPath, canonicalPath);
} catch (err) {
if (err.message === 'invalid MCP lock owner') {
if (!isMcpUpdateRunning()) rmSync(lockPath); // clear malformed lock, then retry
} else throw err;
} Prevention
- Never hand-edit or truncate MCP lock files.
- Ensure lock files are chmod 600 on POSIX so mode checks pass.
- Upgrade the CLI if locks were written by an older version with a different format.
- Confirm the update targets the same canonical config path the lock was created for.
- Only delete a malformed lock after confirming no update process is alive.
When it happens
Trigger: Encountering a lock file at the MCP lock path whose contents are malformed, whose config_path differs from the canonical path being updated, whose started_at is not a round-trippable ISO string, whose token is not a UUIDv4, or whose file mode is too permissive (world/group readable on POSIX).
Common situations: A crashed or buggy older CLI version wrote a lock in an unrecognized format; a user hand-edited or truncated the lock file; files were copied between machines with permissions changed; a different project's lock file sits at a shared path.
Related errors
- ${path} changed during MCP update; refusing overwrite
- ${path} changed during MCP update; refusing removal
- cave_subagent_concurrency_limit_invalid
- cave_subagent_concurrency_limit
- caveman build: package artifact symlink is not lockable: ${J
AI-assisted analysis of JuliusBrussee/caveman@5184b3d11a (2026-09-06).
Data as JSON: /api/errors/8151c7d229155af8.
Report an issue: GitHub.