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

  1. 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
  2. Or restore the file to one of the known states (the pending journal's before_base64 content) if you want recovery to complete untouched
  3. 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

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


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