{"record":{"id":"60dfea83bf11b882","repo":"JuliusBrussee/caveman","slug":"skill-file-changed-during-interrupted-setup-re","errorCode":null,"errorMessage":"${skill.file} changed during interrupted setup; refusing destructive recovery","messagePattern":"(.+?) changed during interrupted setup; refusing destructive recovery","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":2429,"sourceCode":"function sameMcpCommand(left: { command: string; args: string[] } | null, right: { command: string; args: string[] } | null): boolean {\n  return left?.command === right?.command && JSON.stringify(left?.args ?? null) === JSON.stringify(right?.args ?? null);\n}\n\nfunction recoverPendingAgentNativeBundle(agent: \"claude\" | \"codex\"): void {\n  const pending = readPendingAgentNativeBundleJournal(agent);\n  if (!pending) return;\n  // During an upgrade, the committed journal describes transaction-start\n  // state. Keep accepting that exact state until the pending target commits.\n  const committed = readAgentNativeBundleJournal(agent);\n  const committedSkills = new Map(committed?.skills.map((skill) => [skill.file, skill.after_sha256]) ?? []);\n  for (const skill of pending.skills) {\n    const current = fileBytes(skill.file);\n    const before = skill.before_base64 === null ? null : Buffer.from(skill.before_base64, \"base64\");\n    const currentMatchesBefore = current === null ? before === null : before !== null && bytesHash(current) === bytesHash(before);\n    const currentHash = current ? bytesHash(current) : null;\n    const currentMatchesCommitted = currentHash !== null && currentHash === committedSkills.get(skill.file);\n    if (!currentMatchesBefore && !currentMatchesCommitted && currentHash !== skill.after_sha256) {\n      throw new Error(`${skill.file} changed during interrupted setup; refusing destructive recovery`);\n    }\n  }\n  const marker = readMcpServerMarker(agent, \"caveman-cloud\");\n  const markerKnown = !marker\n    || sameMcpCommand(marker, pending.cloud_mcp)\n    || sameMcpCommand(marker, pending.previous_cloud_mcp)\n    || sameMcpCommand(marker, committed?.cloud_mcp ?? null);\n  const hostInstalled = agentNativeCloudMcpMatches(agent, pending.cloud_mcp);\n  const hostPrevious = pending.previous_cloud_mcp\n    ? agentNativeCloudMcpMatches(agent, pending.previous_cloud_mcp)\n    : agentNativeCloudMcpHostAbsent(agent);\n  const hostCommitted = committed ? agentNativeCloudMcpMatches(agent, committed.cloud_mcp) : false;\n  if (!markerKnown || (!hostInstalled && !hostPrevious && !hostCommitted)) {\n    throw new Error(`${agent} caveman-cloud MCP changed during interrupted setup; refusing destructive recovery`);\n  }\n  restoreInstalledAgentNativeBundle(agent, pending);\n  unlinkSync(agentNativeBundleJournalPath(agent, true));\n  process.stderr.write(`${mark(\"warn\")} completed interrupted ${agent} agent-native bundle before continuing\\n`);","sourceCodeStart":2411,"sourceCodeEnd":2447,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/cli/src/index.ts#L2411-L2447","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import { createHash } from \"node:crypto\";\nfunction skillInKnownState(file: string, pending: { before_base64: string | null; after_sha256: string }): boolean {\n  let bytes: Buffer | null = null;\n  try { bytes = readFileSync(file); } catch { return true; }\n  const h = createHash(\"sha256\").update(bytes).digest(\"hex\");\n  if (h === pending.after_sha256) return true;\n  if (pending.before_base64 === null) return false;\n  const bh = createHash(\"sha256\").update(Buffer.from(pending.before_base64, \"base64\")).digest(\"hex\");\n  return h === bh;\n}","typeGuard":"function isSkillChangedDuringSetupError(e: unknown): boolean {\n  return e instanceof Error && e.message.endsWith(\"changed during interrupted setup; refusing destructive recovery\");\n}","tryCatchPattern":"try {\n  runCavemanAgentCommand(\"claude\");\n} catch (e) {\n  if (isSkillChangedDuringSetupError(e)) {\n    // e.message names the file — back it up, remove it, re-run, then re-apply edits\n  } else throw e;\n}","preventionTips":["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"],"tags":["recovery","skills","safety","agent-native"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}