{"record":{"id":"21408c8e09171ac4","repo":"JuliusBrussee/caveman","slug":"skill-file-changed-during-interrupted-removal","errorCode":null,"errorMessage":"${skill.file} changed during interrupted removal; refusing destructive recovery","messagePattern":"(.+?) changed during interrupted removal; refusing destructive recovery","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":2496,"sourceCode":"  installAgentNativeCloudMcp(agent, journal.cloud_mcp);\n  installAgentNativeBundleSkills(agent, journal);\n  verifyAgentNativeBundle(agent, journal, journal.cloud_mcp);\n  atomicWriteFile(agentNativeBundleJournalPath(agent), Buffer.from(JSON.stringify(journal, null, 2) + \"\\n\"));\n}\n\nfunction recoverPendingAgentNativeRemoval(agent: \"claude\" | \"codex\"): void {\n  const pending = readPendingAgentNativeBundleRemoval(agent);\n  if (!pending) return;\n  if (agentNativeBundleRemovalCompleted(agent, pending)) {\n    try { unlinkSync(agentNativeBundleJournalPath(agent)); } catch { /* removal already committed */ }\n    unlinkSync(agentNativeBundleRemovalJournalPath(agent));\n    process.stderr.write(`${mark(\"warn\")} completed interrupted ${agent} agent-native bundle removal\\n`);\n    return;\n  }\n  for (const skill of pending.skills) {\n    const current = fileBytes(skill.file);\n    if (current && bytesHash(current) !== skill.after_sha256 && !skillMatchesBefore(skill)) {\n      throw new Error(`${skill.file} changed during interrupted removal; refusing destructive recovery`);\n    }\n  }\n  const marker = readMcpServerMarker(agent, \"caveman-cloud\");\n  const markerKnown = !marker || sameMcpCommand(marker, pending.cloud_mcp) || sameMcpCommand(marker, pending.previous_cloud_mcp);\n  const hostIsInstalled = agentNativeCloudMcpMatches(agent, pending.cloud_mcp);\n  const hostIsPrevious = pending.previous_cloud_mcp\n    ? agentNativeCloudMcpMatches(agent, pending.previous_cloud_mcp)\n    : agentNativeCloudMcpHostAbsent(agent);\n  if (!markerKnown || (!hostIsInstalled && !hostIsPrevious)) {\n    throw new Error(`${agent} caveman-cloud MCP changed during interrupted removal; refusing destructive recovery`);\n  }\n  restoreInstalledAgentNativeBundle(agent, pending);\n  unlinkSync(agentNativeBundleRemovalJournalPath(agent));\n  process.stderr.write(`${mark(\"warn\")} rolled back interrupted ${agent} agent-native bundle removal before continuing\\n`);\n}\n\nfunction preflightAgentNativeSetup(agent: \"claude\" | \"codex\"): void {\n  recoverPendingAgentNativeRemoval(agent);","sourceCodeStart":2478,"sourceCodeEnd":2514,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/cli/src/index.ts#L2478-L2514","documentation":"During recovery of an interrupted removal, each journaled skill file is checked: if it still exists, its hash must equal the recorded after_sha256, or the file must byte-equal the pre-install original (skillMatchesBefore). A file that exists but matches neither was edited by the user after the interrupted removal, and deleting it (completing the removal) would destroy those edits — so recovery refuses.","triggerScenarios":"Kill an uninstall mid-way, edit one of the still-present Caveman skill files, then run any command that triggers recoverPendingAgentNativeRemoval.","commonSituations":"User customized a skill thinking uninstall was complete; an agent tool rewrote SKILL.md bodies; pixel convert ran over the skills directory in between.","solutions":["Back up your edited skill file, delete it (or restore the original/after content), and re-run so removal completes","Or keep your edits and remove the pending removal journal plus the skill files manually, accepting you own the cleanup"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"function skillSafeToRemove(file: string, afterSha: string, beforeB64: string | null): boolean {\n  let bytes: Buffer;\n  try { bytes = readFileSync(file); } catch { return true; } // gone = fine\n  const h = createHash(\"sha256\").update(bytes).digest(\"hex\");\n  if (h === afterSha) return true;\n  if (beforeB64 === null) return false;\n  return bytes.equals(Buffer.from(beforeB64, \"base64\"));\n}","typeGuard":"function isSkillChangedDuringRemovalError(e: unknown): boolean {\n  return e instanceof Error && e.message.endsWith(\"changed during interrupted removal; refusing destructive recovery\");\n}","tryCatchPattern":"try {\n  runCavemanAgentCommand(\"codex\");\n} catch (e) {\n  if (isSkillChangedDuringRemovalError(e)) {\n    // message names the file — back it up, move it aside, re-run to complete removal\n  } else throw e;\n}","preventionTips":["Do not edit skill files between an interrupted uninstall and its recovery","Keep personal skill content in separate directories from the caveman suite"],"tags":["recovery","skills","uninstall","safety"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}