JuliusBrussee/caveman · error

Cave Build lock changed during validation; refusing Claude l

Error message

Cave Build lock changed during validation; refusing Claude launch before model spend

What it means

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.

Source

Thrown at packages/cli/src/index.ts:5051

    throw error;
  }
  const checker = process.env.CAVEMAN_AGENT_BIN || which("caveman-agent");
  if (!checker) {
    throw new Error("Cave Build lock exists but caveman-agent checker is unavailable; refusing Claude launch before model spend");
  }
  const invocation = portableInvocation(checker, ["check"]);
  const checked = spawnSync(invocation.command, invocation.args, {
    cwd: process.cwd(),
    env: process.env,
    encoding: "utf8",
  });
  if (checked.error || checked.status !== 0) {
    const detail = String(checked.stderr || checked.error?.message || "lock check failed").trim();
    throw new Error(`Cave Build lock is stale or invalid; refusing Claude launch before model spend: ${detail}`);
  }
  const rawAfterCheck = readFileSync(lockPath, "utf8");
  if (rawAfterCheck !== rawBeforeCheck) {
    throw new Error("Cave Build lock changed during validation; refusing Claude launch before model spend");
  }
  const lock = JSON.parse(rawAfterCheck) as { harness?: { id?: unknown } };
  if (lock.harness?.id !== "claude") {
    throw new Error(
      "Cave Build is Pi-specific; refusing to attach its identity to Claude Code execution",
    );
  }
  throw new Error(
    "Claude-specific Cave Build execution is unavailable until model, reasoning, budget, recovery, and wire selectors are enforced",
  );
}

function firstEnvSecret(env: NodeJS.ProcessEnv, keys: string[]): string | undefined {
  for (const key of keys) {
    const value = env[key];
    if (typeof value === "string" && value.trim()) return value.trim();
  }
  return undefined;

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Ensure only one caveman-guarded launch runs per project at a time
  2. Wait for the active Cave Build session to finish or release its lock, then relaunch
  3. If no session is active, remove the lock file when nothing is rewriting it, then relaunch
Defensive patterns

Strategy: retry

Try / catch

let lastError: unknown;
for (let attempt = 0; attempt < 3; attempt++) {
  try { claudeCaveBuildEnv(); break; }
  catch (error) {
    if (!/lock changed during validation/.test((error as Error).message)) throw error;
    lastError = error;
    await waitForLockQuiescence();  // e.g. mtime stable for 2s
  }
}
if (lastError) throw lastError;

Prevention

When it happens

Trigger: Another process (live Cave Build session, cleanup script, second launcher) rewrites .caveman/agent.lock.json while `caveman-agent check` is executing.

Common situations: Two Claude/caveman launches racing in the same project; a Cave Build session renewing its lock heartbeat; an external tool touching lock files.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/4f1c6497be3af632. Report an issue: GitHub.