{"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":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":5051,"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":5033,"sourceCodeEnd":5069,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/cli/src/index.ts#L5033-L5069","documentation":"TOCTOU guard in claudeCaveBuildEnv: the lock file is read before and after the checker subprocess runs; if rawAfterCheck !== rawBeforeCheck the lock was modified concurrently and validation cannot be trusted, so launch is refused before model spend.","triggerScenarios":"Another process (live Cave Build session, cleanup script, second launcher) rewrites .caveman/agent.lock.json while `caveman-agent check` is executing.","commonSituations":"Two Claude/caveman launches racing in the same project; a Cave Build session renewing its lock heartbeat; an external tool touching lock files.","solutions":["Ensure only one caveman-guarded launch runs per project at a time","Wait for the active Cave Build session to finish or release its lock, then relaunch","If no session is active, remove the lock file when nothing is rewriting it, then relaunch"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"let lastError: unknown;\nfor (let attempt = 0; attempt < 3; attempt++) {\n  try { claudeCaveBuildEnv(); break; }\n  catch (error) {\n    if (!/lock changed during validation/.test((error as Error).message)) throw error;\n    lastError = error;\n    await waitForLockQuiescence();  // e.g. mtime stable for 2s\n  }\n}\nif (lastError) throw lastError;","preventionTips":["Serialize caveman-guarded launches per project (flock or CI job lock)","Do not run lock-touching cleanup scripts concurrently with launches","Treat this as a race: brief retry after the writer finishes is correct; deletion is not"],"tags":["cave-build","toctou","race-condition","lock"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}