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

  1. Verify no MCP update is actually running, then remove the malformed lock file and retry the update.
  2. Check the lock file mode on POSIX (chmod 600 lock and owner files) if permissions are the violation.
  3. Ensure you are operating on the same canonical config path the lock was created for.
  4. 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

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


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