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
- Ensure no other caveman CLI instance is running in the same directory (check for duplicate processes)
- Retry the launch — the race is transient if the concurrent writer finished
- Serialize launches: run one at a time per working directory, or use separate working directories/lock scopes
- Pause file-sync tools (Dropbox/OneDrive) or exclude the lock path from sync
- 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
- Run only one caveman-gated launch per working directory at a time
- Exclude lock files from Dropbox/OneDrive/rsync sync
- Stagger lock-refresh watchdogs so they cannot fire during a launch
- Use separate working directories for concurrent harness sessions
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
- Cave Build is Pi-specific; refusing to attach its identity…
- Cave Build lock exists but caveman-agent checker is…
- Cave Build lock is stale or invalid; refusing Claude launch…
- changed while planning MCP update; refusing overwrite
- memory changed during supersede
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)