{"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":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":2749,"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":2731,"sourceCodeEnd":2767,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L2731-L2767","documentation":"During recovery of an interrupted agent-native bundle setup, the CLI verifies each journaled skill file still matches what the journal recorded (either its pre-setup content, its committed hash, or its after-hash). If none match, the file was modified outside the setup process, and the CLI refuses to run destructive recovery steps over unknown content. This guards against clobbering user edits or unrelated files.","triggerScenarios":"Running setup for claude/codex while a pending bundle journal exists (from a previously interrupted run) and a journaled skill file at skill.file has content that matches neither its before_base64 snapshot, the committed skills map hash, nor after_sha256.","commonSituations":"User manually edited or reverted a skill file between the interrupted setup and the retry; another tool (git checkout, sync client) rewrote the file; a different Caveman version wrote the journal with different content hashes.","solutions":["Inspect the journaled skill file and restore it to either its pre-setup content or the canonical content of the version being installed","Delete the pending bundle journal file if you accept the current file contents and want setup to proceed fresh","Re-run the interrupted setup to completion so the journal is consumed and removed"],"exampleFix":"// before\nthrow new Error(`${skill.file} changed during interrupted setup; refusing destructive recovery`);\n// after\n// restore the file to its journaled before-state, then delete the journal:\nwriteFileSync(skill.file, Buffer.from(skill.before_base64, 'base64'));\nunlinkSync(agentNativeBundleJournalPath(agent, true));","handlingStrategy":"validation","validationCode":"const current = readFileSync(skillFile); const hash = createHash('sha256').update(current).digest('hex');\nconst journal = JSON.parse(readFileSync(journalPath,'utf8'));\nif (hash !== journal.after_sha256 && !current.equals(Buffer.from(journal.before_base64,'base64'))) {\n  console.error('Skill file diverged from journal; resolve manually before re-running setup.');\n}","typeGuard":null,"tryCatchPattern":"try {\n  runSetup(agent);\n} catch (e) {\n  if (String(e.message).includes('refusing destructive recovery')) {\n    // inspect the named file, restore or back it up, then retry\n  }\n}","preventionTips":["Do not hand-edit files under the agent-native skill directory","Re-run or complete an interrupted setup before modifying installed files","Keep skill directories out of sync tools that rewrite files","Back up the journal file before manual cleanup"],"tags":["journal-integrity","skill-files","recovery","cli"],"backgroundTag":"checksum-mismatch","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}