{"record":{"id":"4f1c6497be3af632","repo":"JuliusBrussee/caveman","slug":"cave-build-lock-changed-during-validation-refusin","errorCode":null,"errorMessage":"Cave Build lock changed during validation; refusing Claude launch before model spend","messagePattern":"Cave Build lock changed during validation; refusing Claude launch before model spend","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":5566,"sourceCode":"    throw error;\n  }\n  const checker = process.env.CAVEMAN_AGENT_BIN || which(\"caveman-agent\");\n  if (!checker) {\n    throw new Error(\"Cave Build lock exists but caveman-agent checker is unavailable; refusing Claude launch before model spend\");\n  }\n  const invocation = portableInvocation(checker, [\"check\"]);\n  const checked = spawnSync(invocation.command, invocation.args, {\n    cwd: process.cwd(),\n    env: process.env,\n    encoding: \"utf8\",\n  });\n  if (checked.error || checked.status !== 0) {\n    const detail = String(checked.stderr || checked.error?.message || \"lock check failed\").trim();\n    throw new Error(`Cave Build lock is stale or invalid; refusing Claude launch before model spend: ${detail}`);\n  }\n  const rawAfterCheck = readFileSync(lockPath, \"utf8\");\n  if (rawAfterCheck !== rawBeforeCheck) {\n    throw new Error(\"Cave Build lock changed during validation; refusing Claude launch before model spend\");\n  }\n  const lock = JSON.parse(rawAfterCheck) as { harness?: { id?: unknown } };\n  if (lock.harness?.id !== \"claude\") {\n    throw new Error(\n      \"Cave Build is Pi-specific; refusing to attach its identity to Claude Code execution\",\n    );\n  }\n  throw new Error(\n    \"Claude-specific Cave Build execution is unavailable until model, reasoning, budget, recovery, and wire selectors are enforced\",\n  );\n}\n\nfunction firstEnvSecret(env: NodeJS.ProcessEnv, keys: string[]): string | undefined {\n  for (const key of keys) {\n    const value = env[key];\n    if (typeof value === \"string\" && value.trim()) return value.trim();\n  }\n  return undefined;","sourceCodeStart":5548,"sourceCodeEnd":5584,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L5548-L5584","documentation":"During pre-launch validation the CLI snapshots the raw lock file bytes, runs the caveman-agent checker, then re-reads the file. If the contents changed between the two reads, another process is concurrently mutating the lock, so the CLI aborts the Claude launch rather than validating against a moving target.","triggerScenarios":"The lock file at lockPath is modified between readFileSync(rawBeforeCheck) and readFileSync(rawAfterCheck) — e.g. another CLI instance refreshed/renewed the lock, a background watchdog rewrote it, or a cleanup process deleted-and-recreated it while `caveman-agent check` was running.","commonSituations":"Two terminal sessions sharing the same working directory both trying to launch Claude through the Cave Build gate; a lock-refresh interval that fires mid-validation; an editor or sync tool (Dropbox/rsync) rewriting the lock during the check window.","solutions":["Ensure no other caveman CLI instance is running in the same directory (check for duplicate processes)","Retry the launch — the race is transient if the concurrent writer finished","Serialize launches: run one at a time per working directory, or use separate working directories/lock scopes","Pause file-sync tools (Dropbox/OneDrive) or exclude the lock path from sync","If a watchdog renews locks, increase its interval or gate launches against it"],"exampleFix":"// before: two terminals in same dir\n$ terminal-a: caveman-cli launch claude &\n$ terminal-b: caveman-cli launch claude   # lock rewritten mid-check\n// after\n$ flock /tmp/cave-launch.lock caveman-cli launch claude","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"async function launchWithRetry(max = 3) {\n  for (let i = 0; i < max; i++) {\n    try { return cli.launchClaude(); }\n    catch (err) {\n      if ((err as Error).message.includes('lock changed during validation') && i < max - 1) {\n        await new Promise(r => setTimeout(r, 250)); continue;\n      }\n      throw err;\n    }\n  }\n}","preventionTips":["Run only one caveman-gated launch per working directory at a time","Exclude lock files from Dropbox/OneDrive/rsync sync","Stagger lock-refresh watchdogs so they cannot fire during a launch","Use separate working directories for concurrent harness sessions"],"tags":["lockfile","race-condition","concurrency","cave-build"],"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"}