JuliusBrussee/caveman · error · Error
${skill.file} changed during interrupted setup; refusing des
Error message
${skill.file} changed during interrupted setup; refusing destructive recovery What it means
During recovery of an interrupted setup, for each skill in the pending journal the current on-disk bytes must match one of three known states: the recorded 'before' content (hash of before_base64), the committed journal's after_sha256, or the pending after_sha256. If the file exists but hashes to none of these, someone edited it between the crash and the recovery, and the CLI refuses to overwrite those edits — destructive recovery is blocked.
Source
Thrown at packages/cli/src/index.ts:2429
function sameMcpCommand(left: { command: string; args: string[] } | null, right: { command: string; args: string[] } | null): boolean {
return left?.command === right?.command && JSON.stringify(left?.args ?? null) === JSON.stringify(right?.args ?? null);
}
function recoverPendingAgentNativeBundle(agent: "claude" | "codex"): void {
const pending = readPendingAgentNativeBundleJournal(agent);
if (!pending) return;
// During an upgrade, the committed journal describes transaction-start
// state. Keep accepting that exact state until the pending target commits.
const committed = readAgentNativeBundleJournal(agent);
const committedSkills = new Map(committed?.skills.map((skill) => [skill.file, skill.after_sha256]) ?? []);
for (const skill of pending.skills) {
const current = fileBytes(skill.file);
const before = skill.before_base64 === null ? null : Buffer.from(skill.before_base64, "base64");
const currentMatchesBefore = current === null ? before === null : before !== null && bytesHash(current) === bytesHash(before);
const currentHash = current ? bytesHash(current) : null;
const currentMatchesCommitted = currentHash !== null && currentHash === committedSkills.get(skill.file);
if (!currentMatchesBefore && !currentMatchesCommitted && currentHash !== skill.after_sha256) {
throw new Error(`${skill.file} changed during interrupted setup; refusing destructive recovery`);
}
}
const marker = readMcpServerMarker(agent, "caveman-cloud");
const markerKnown = !marker
|| sameMcpCommand(marker, pending.cloud_mcp)
|| sameMcpCommand(marker, pending.previous_cloud_mcp)
|| sameMcpCommand(marker, committed?.cloud_mcp ?? null);
const hostInstalled = agentNativeCloudMcpMatches(agent, pending.cloud_mcp);
const hostPrevious = pending.previous_cloud_mcp
? agentNativeCloudMcpMatches(agent, pending.previous_cloud_mcp)
: agentNativeCloudMcpHostAbsent(agent);
const hostCommitted = committed ? agentNativeCloudMcpMatches(agent, committed.cloud_mcp) : false;
if (!markerKnown || (!hostInstalled && !hostPrevious && !hostCommitted)) {
throw new Error(`${agent} caveman-cloud MCP changed during interrupted setup; refusing destructive recovery`);
}
restoreInstalledAgentNativeBundle(agent, pending);
unlinkSync(agentNativeBundleJournalPath(agent, true));
process.stderr.write(`${mark("warn")} completed interrupted ${agent} agent-native bundle before continuing\n`);View on GitHub (pinned to 27d5a3981a)
Solutions
- Back up the edited skill file, then let setup overwrite it (delete or move the file) and re-apply your edits afterwards on top of the canonical body
- Or restore the file to one of the known states (the pending journal's before_base64 content) if you want recovery to complete untouched
- Or abandon the pending transaction deliberately: remove the pending journal and re-run setup, which will treat the file as unjournaled and re-evaluate it
Defensive patterns
Strategy: validation
Validate before calling
import { createHash } from "node:crypto";
function skillInKnownState(file: string, pending: { before_base64: string | null; after_sha256: string }): boolean {
let bytes: Buffer | null = null;
try { bytes = readFileSync(file); } catch { return true; }
const h = createHash("sha256").update(bytes).digest("hex");
if (h === pending.after_sha256) return true;
if (pending.before_base64 === null) return false;
const bh = createHash("sha256").update(Buffer.from(pending.before_base64, "base64")).digest("hex");
return h === bh;
} Type guard
function isSkillChangedDuringSetupError(e: unknown): boolean {
return e instanceof Error && e.message.endsWith("changed during interrupted setup; refusing destructive recovery");
} Try / catch
try {
runCavemanAgentCommand("claude");
} catch (e) {
if (isSkillChangedDuringSetupError(e)) {
// e.message names the file — back it up, remove it, re-run, then re-apply edits
} else throw e;
} Prevention
- Do not edit Caveman-installed skill files while a setup is interrupted
- Keep custom skill content under your own skill names, not the caveman suite paths
- Re-run setup immediately after a crash so pending transactions settle before you touch files
When it happens
Trigger: Kill `caveman setup` mid-bundle-install, then edit one of the ~/.claude/skills/<name>/SKILL.md or ~/.codex/skills files, then run any agent command that triggers recoverPendingAgentNativeBundle.
Common situations: User customized a Caveman-installed skill after an interrupted setup; an editor/agent auto-formatted the SKILL.md; a third-party skills tool rewrote the file.
Related errors
- ${agent} caveman-cloud MCP changed during interrupted setup;
- ${skill.file} changed during interrupted removal; refusing d
- ${file} changed after setup; refusing to overwrite user skil
- cannot read pending ${agent} agent-native bundle journal: ${
- cannot read pending ${agent} agent-native removal journal: $
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/60dfea83bf11b882.
Report an issue: GitHub.