{"record":{"id":"8151c7d229155af8","repo":"JuliusBrussee/caveman","slug":"invalid-mcp-lock-owner","errorCode":null,"errorMessage":"invalid MCP lock owner","messagePattern":"invalid MCP lock owner","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":7904,"sourceCode":"      try {\n        const lockStat = lstatSync(lock);\n        const ownerPath = join(lock, \"owner.json\");\n        const ownerStat = lstatSync(ownerPath);\n        const existing = JSON.parse(readFileSync(ownerPath, \"utf8\")) as Record<string, unknown>;\n        const keys = [\"config_path\", \"pid\", \"schema_version\", \"started_at\", \"token\"];\n        if (!lockStat.isDirectory() || lockStat.isSymbolicLink()\n          || !ownerStat.isFile() || ownerStat.isSymbolicLink()\n          || readdirSync(lock).sort().join(\"\\0\") !== \"owner.json\"\n          || Object.keys(existing).sort().join(\"\\0\") !== keys.join(\"\\0\")\n          || existing.schema_version !== 1\n          || typeof existing.pid !== \"number\" || !Number.isInteger(existing.pid) || existing.pid <= 1\n          || typeof existing.token !== \"string\"\n          || !/^[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)\n          || existing.config_path !== canonicalPath\n          || typeof existing.started_at !== \"string\"\n          || new Date(existing.started_at).toISOString() !== existing.started_at\n          || process.platform !== \"win32\" && ((lockStat.mode | ownerStat.mode) & 0o077) !== 0) {\n          throw new Error(\"invalid MCP lock owner\");\n        }\n        try { process.kill(existing.pid, 0); }\n        catch (probeError) { stale = (probeError as NodeJS.ErrnoException).code === \"ESRCH\"; }\n      } catch {\n        // Populated claim is durable before publication, so malformed or\n        // ownerless lock can never be our crash residue. Never delete it.\n        stale = false;\n      }\n      if (!stale) throw new Error(`MCP config change already running for ${canonicalPath}`);\n      const quarantine = `${lock}.stale-${token}`;\n      try {\n        renameSync(lock, quarantine);\n        renameSync(claim, lock);\n        fsyncParentDirectory(lock);\n        process.stderr.write(`${mark(\"warn\")} reclaimed stale MCP config lock for ${canonicalPath}\\n`);\n      } catch {\n        throw new Error(`MCP config change already running for ${canonicalPath}`);\n      } finally {","sourceCodeStart":7886,"sourceCodeEnd":7922,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L7886-L7922","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nls -l ~/.caveman/mcp.lock  # -rw-rw-rw- (world-writable)\n// after\nchmod 600 ~/.caveman/mcp.lock && caveman mcp sync","handlingStrategy":"validation","validationCode":"import { statSync, readFileSync } from \"node:fs\";\nconst st = statSync(lockPath);\nif ((st.mode & 0o077) !== 0) console.error(\"lock file too permissive; chmod 600 before running\");\nconst owner = JSON.parse(readFileSync(lockPath, \"utf8\"));\nif (!/^[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\");","typeGuard":"const isValidClaim = (o: unknown): o is { pid: number; token: string; config_path: string; started_at: string } =>\n  typeof o === \"object\" && o !== null && \"token\" in o && /^[0-9a-f-]{36}$/.test((o as any).token);","tryCatchPattern":"try { mcpSync(); } catch (e) { if (String(e.message).includes(\"invalid MCP lock owner\")) { inspectAndRebuildLock(lockPath); } }","preventionTips":["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."],"tags":["locking","validation","mcp"],"backgroundTag":"internal-invariant-violation","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}