JuliusBrussee/caveman · error · Error

Cave Build lock changed during validation; refusing Claude…

Error message

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

What it means

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.

Solutions

  1. Ensure no other caveman CLI instance is running in the same directory (check for duplicate processes)
  2. Retry the launch — the race is transient if the concurrent writer finished
  3. Serialize launches: run one at a time per working directory, or use separate working directories/lock scopes
  4. Pause file-sync tools (Dropbox/OneDrive) or exclude the lock path from sync
  5. If a watchdog renews locks, increase its interval or gate launches against it

Example fix

// before: two terminals in same dir
$ terminal-a: caveman-cli launch claude &
$ terminal-b: caveman-cli launch claude   # lock rewritten mid-check
// after
$ flock /tmp/cave-launch.lock caveman-cli launch claude
Defensive patterns

Strategy: retry

Try / catch

async function launchWithRetry(max = 3) {
  for (let i = 0; i < max; i++) {
    try { return cli.launchClaude(); }
    catch (err) {
      if ((err as Error).message.includes('lock changed during validation') && i < max - 1) {
        await new Promise(r => setTimeout(r, 250)); continue;
      }
      throw err;
    }
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/4f1c6497be3af632. Report an issue: GitHub.

Appendix: source

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

    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 3ee70a1026)