JuliusBrussee/caveman · error

Cave Build lock is stale or invalid; refusing Claude launch

Error message

Cave Build lock is stale or invalid; refusing Claude launch before model spend: ${detail}

What it means

The caveman-agent `check` subprocess spawned to validate the Cave Build lock either failed to spawn (checked.error) or exited non-zero (checked.status !== 0). The lock is treated as stale or invalid and Claude launch is refused before any model spend; the lock checker's stderr (or spawn error message) is embedded as detail.

Source

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

  try {
    rawBeforeCheck = readFileSync(lockPath, "utf8");
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
    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) {

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Read the embedded detail — it is the checker's own diagnostic of why the lock is invalid
  2. Update caveman-agent to the matching version via `caveman setup --install`
  3. If the lock is genuinely stale (no live Cave Build session), delete .caveman/agent.lock.json
  4. Verify the binary executes: `caveman-agent check` in the project directory
Defensive patterns

Strategy: try-catch

Validate before calling

import { spawnSync } from "node:child_process";
// validate the lock the same way before launching
const check = spawnSync("caveman-agent", ["check"], { cwd: process.cwd(), encoding: "utf8" });
if (check.error || check.status !== 0) {
  throw new Error(`lock invalid: ${String(check.stderr).trim()}; remove .caveman/agent.lock.json if stale`);
}

Try / catch

try {
  claudeCaveBuildEnv();
} catch (error) {
  const msg = (error as Error).message;
  if (/lock is stale or invalid/.test(msg)) {
    const detail = msg.split(": ").pop()!;
    if (/no active session|unknown lock/i.test(detail)) rmSync(".caveman/agent.lock.json");
    else throw error;  // version/schema problems need caveman-agent updated, not deletion
  } else throw error;
}

Prevention

When it happens

Trigger: Spawning `caveman-agent check` in a project with .caveman/agent.lock.json when the lock file references a dead session/unknown lock id, the checker binary is a version that rejects the lock format, or the process itself cannot execute (exec format error, timeout).

Common situations: Lock left behind by a crashed Cave Build session; caveman-agent version older/newer than the lock schema; corrupted lock JSON; architecture-mismatched binary.

Related errors


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