{"record":{"id":"ba03cba624f6f57a","repo":"JuliusBrussee/caveman","slug":"operation-file-changed-during-interrupted-age","errorCode":null,"errorMessage":"${operation.file} changed during interrupted ${agent} install; refusing recovery","messagePattern":"(.+?) changed during interrupted (.+?) install; refusing recovery","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":6913,"sourceCode":"  if (!pending) return false;\n  const committed = readNativeJournal(agent);\n  if (committed) {\n    if (JSON.stringify(committed) !== JSON.stringify(pending)) {\n      throw new Error(`conflicting committed and pending integration journals for ${agent}; refusing recovery`);\n    }\n    unlinkSync(nativePendingJournalPath(agent));\n    return false;\n  }\n  const current = pending.operations.map((operation) => ({ file: operation.file, bytes: fileBytes(operation.file) }));\n  const restored = pending.operations.map((operation) => {\n    const before = nativeBackupBytes(operation);\n    const now = fileBytes(operation.file);\n    const isInstalled = Boolean(now && bytesHash(now) === operation.after_sha256);\n    const isBefore = operation.before_exists\n      ? Boolean(now && operation.before_sha256 && bytesHash(now) === operation.before_sha256)\n      : now === null;\n    if (!isInstalled && !isBefore) {\n      throw new Error(`${operation.file} changed during interrupted ${agent} install; refusing recovery`);\n    }\n    return { file: operation.file, bytes: before };\n  });\n  try {\n    for (const item of restored) writeNativeRestoration(item.file, item.bytes);\n    unlinkSync(nativePendingJournalPath(agent));\n  } catch (error) {\n    for (const item of current) {\n      try { writeNativeRestoration(item.file, item.bytes); } catch { /* original error remains authority */ }\n    }\n    throw error;\n  }\n  process.stderr.write(`${mark(\"warn\")} recovered interrupted ${agent} integration change before continuing\\n`);\n  return true;\n}\n\nfunction nativeMutationsFor(agent: NativeAgent, gw: string, mcpBinary: string | undefined): NativeMutation[] {\n  return agent === \"claude\"","sourceCodeStart":6895,"sourceCodeEnd":6931,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/cli/src/index.ts#L6895-L6931","documentation":"During recovery of an interrupted native install, each journaled file must hash-match either its recorded post-install content (after_sha256) or its pre-install content (before_sha256 / before_exists=false meaning absent). If a file matches neither, something modified it between the crash and recovery, and restoring backups would clobber unknown changes — so recovery refuses. The same guard protects the rollback path.","triggerScenarios":"Recovery runs after an interrupted install and a target file (e.g. an agent settings file) was edited by the user or by the agent itself since the crash; fileBytes(file) hashed against both before_sha256 and after_sha256 fails both checks.","commonSituations":"User opens and saves their Claude/Codex settings file after a crashed install, an agent rewrites its own config at startup, editors that touch mtime/content (formatters, sync tools), or a partially-restored backup from a prior recovery attempt.","solutions":["Identify the file named in the error and diff it against the agent's expected config and your recent edits.","Back up the changed file, then either re-run the interrupted install (which overwrites toward after_sha256) or restore the pre-install content manually and re-run.","Remove the pending journal only after you have reconciled the file, so recovery stops tripping on it."],"exampleFix":"# before: user edits settings between crash and recovery\ncaveman recover   # refuses: settings.json changed during interrupted install\n# after: reconcile then re-run\ncp settings.json settings.json.bak && caveman install claude","handlingStrategy":"validation","validationCode":"import { createHash } from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\n\nfunction fileMatchesJournal(file: string, before?: string, after?: string): boolean {\n  let bytes: Buffer;\n  try { bytes = readFileSync(file); } catch { return before === undefined; }\n  const sha = createHash(\"sha256\").update(bytes).digest(\"hex\");\n  return sha === after || sha === before;\n}","typeGuard":null,"tryCatchPattern":"try {\n  runRecovery(agent);\n} catch (e) {\n  if (e instanceof Error && e.message.includes(\"changed during interrupted\")) {\n    const file = e.message.split(\" \")[0]; // first token is the file path\n    backupAndReconcile(file); // operator decision, then re-run install\n    return runInstall(agent);\n  }\n  throw e;\n}","preventionTips":["Do not edit agent settings files between a crashed install and recovery.","Stop the agent process before running install/recovery so it cannot rewrite its config concurrently.","Keep the pending journal until state is verified consistent; deleting it loses recovery info."],"tags":["recovery","integrity","hashing","cli"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}