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
- 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
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
- 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
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
- lock disappeared during validation
- Cave Build lock exists but caveman-agent checker is unavaila
- Cave Build lock is stale or invalid; refusing Claude launch
- Cave Build is Pi-specific; refusing to attach its identity t
- cave_stale_lock:registration
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/4f1c6497be3af632.
Report an issue: GitHub.