{"record":{"id":"d06ab257ebc04684","repo":"JuliusBrussee/caveman","slug":"hermes-key-replace-block-block-changed","errorCode":null,"errorMessage":"Hermes ${key.replace(\"_block\", \"\")} block changed after enable; refusing destructive disable","messagePattern":"Hermes (.+?) block changed after enable; refusing destructive disable","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":8415,"sourceCode":"  if (operation.kind === \"aider-core\") {\n    throw new Error(`${operation.file} changed after enable; refusing destructive disable`);\n  }\n  if (operation.kind === \"hermes-plugin-manifest\" || operation.kind === \"hermes-plugin-init\") {\n    throw new Error(`${operation.file} changed after enable; refusing destructive disable`);\n  }\n  if (operation.kind === \"hermes-config\") {\n    let text = current.toString(\"utf8\");\n    const routeBlock = operation.owned?.route_block;\n    const previousRouteLines = operation.owned?.previous_route_lines;\n    if (typeof routeBlock !== \"string\" || !Array.isArray(previousRouteLines)) {\n      throw new Error(\"Hermes integration journal lacks owned routing block\");\n    }\n    if (!text.includes(routeBlock)) throw new Error(\"Hermes routing block changed after enable; refusing destructive disable\");\n    text = text.replace(routeBlock, previousRouteLines.filter((line): line is string => typeof line === \"string\").join(\"\\n\"));\n    for (const key of [\"plugin_block\", \"mcp_block\"] as const) {\n      const block = operation.owned?.[key];\n      if (block === null || block === undefined) continue;\n      if (typeof block !== \"string\" || !text.includes(block)) throw new Error(`Hermes ${key.replace(\"_block\", \"\")} block changed after enable; refusing destructive disable`);\n      text = text.replace(block, \"\");\n    }\n    return Buffer.from(text);\n  }\n  const rootBlock = operation.owned?.root_block;\n  const tablesBlock = operation.owned?.tables_block;\n  if (typeof rootBlock !== \"string\" || typeof tablesBlock !== \"string\") throw new Error(\"Codex integration journal lacks owned blocks\");\n  let text = current.toString(\"utf8\");\n  for (const [block, begin] of [[rootBlock, CODEX_NATIVE_ROOT_BEGIN], [tablesBlock, CODEX_NATIVE_TABLES_BEGIN]] as const) {\n    if (text.includes(begin) && !text.includes(block)) throw new Error(\"Codex Caveman config block changed after enable; refusing destructive disable\");\n    text = text.replace(`${block}\\n\\n`, \"\").replace(`\\n\\n${block}\\n`, \"\\n\").replace(block, \"\");\n  }\n  return Buffer.from(text);\n}\n\nfunction writeNativeRestoration(file: string, bytes: Buffer | null): void {\n  if (bytes) {\n    atomicWriteFile(file, bytes);","sourceCodeStart":8397,"sourceCodeEnd":8433,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L8397-L8433","documentation":"Alongside the routing block, Hermes disable also removes the journaled plugin_block and mcp_block if present. If a journaled block is not a string or is no longer found in the file, the tool throws with the block name (plugin/mcp) interpolated. Same protection principle: never delete text that no longer matches what was installed.","triggerScenarios":"Running disable with kind === \"hermes-config\" when operation.owned[key] (key = plugin_block or mcp_block) is present but either not a string or not found via text.includes(block).","commonSituations":"User edited the embedded Caveman plugin registration or MCP snippet after enable; Hermes rewrote the manifest merging the snippet; partial manual uninstall removed one block but not the journal.","solutions":["Restore the plugin/mcp block to its exact journaled content, then re-run disable.","Manually delete both the Caveman blocks from the Hermes config and clear the journal entry.","Re-run enable to rewrite all blocks and refresh the journal, then disable."],"exampleFix":"// before: mcp block edited after enable\nplugins: [caveman, extra]  # modified\n// after: exact journaled block restored\nplugins: [caveman]","handlingStrategy":"validation","validationCode":"const text = fs.readFileSync(hermesConfig, 'utf8');\nfor (const key of ['plugin_block', 'mcp_block']) {\n  const block = journal.owned?.[key];\n  if (block !== null && block !== undefined && (typeof block !== 'string' || !text.includes(block))) {\n    throw new Error(`${key} drift detected; restore before disable`);\n  }\n}","typeGuard":"const optionalBlockIntact = (text, owned, key) => {\n  const b = owned?.[key];\n  return b == null || (typeof b === 'string' && text.includes(b));\n};","tryCatchPattern":"try {\n  await caveman.disable(['hermes']);\n} catch (err) {\n  const m = err.message.match(/Hermes (\\w+) block changed/);\n  if (m) console.error(`Restore the Hermes ${m[1]} block to its journaled form or delete it manually.`);\n  else throw err;\n}","preventionTips":["Keep the plugin and MCP snippets untouched between enable and disable.","Re-run Hermes manifest regeneration only after Caveman disable completes.","Commit the config before enable for easy diff/restore.","If one block was already edited, restore both blocks before retrying disable."],"tags":["config","hermes","disable-safety"],"backgroundTag":"conflicting-config-options","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"}