JuliusBrussee/caveman · error
Hermes block changed after enable; refusing destructive…
Error message
Hermes ${key.replace("_block", "")} block changed after enable; refusing destructive disable What it means
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.
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.
Example fix
// before: mcp block edited after enable plugins: [caveman, extra] # modified // after: exact journaled block restored plugins: [caveman]
Defensive patterns
Strategy: validation
Validate before calling
const text = fs.readFileSync(hermesConfig, 'utf8');
for (const key of ['plugin_block', 'mcp_block']) {
const block = journal.owned?.[key];
if (block !== null && block !== undefined && (typeof block !== 'string' || !text.includes(block))) {
throw new Error(`${key} drift detected; restore before disable`);
}
} Type guard
const optionalBlockIntact = (text, owned, key) => {
const b = owned?.[key];
return b == null || (typeof b === 'string' && text.includes(b));
}; Try / catch
try {
await caveman.disable(['hermes']);
} catch (err) {
const m = err.message.match(/Hermes (\w+) block changed/);
if (m) console.error(`Restore the Hermes ${m[1]} block to its journaled form or delete it manually.`);
else throw err;
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Understand the failure class
Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.
Related errors
- Hermes routing block changed after enable; refusing…
- Aider Caveman config block changed after enable; refusing…
- existing Hermes Caveman native block is unjournaled; run…
- Hermes integration journal lacks owned routing block
- Hermes model provider/base_url keys are duplicated…
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/d06ab257ebc04684.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/index.ts:8415
if (operation.kind === "aider-core") {
throw new Error(`${operation.file} changed after enable; refusing destructive disable`);
}
if (operation.kind === "hermes-plugin-manifest" || operation.kind === "hermes-plugin-init") {
throw new Error(`${operation.file} changed after enable; refusing destructive disable`);
}
if (operation.kind === "hermes-config") {
let text = current.toString("utf8");
const routeBlock = operation.owned?.route_block;
const previousRouteLines = operation.owned?.previous_route_lines;
if (typeof routeBlock !== "string" || !Array.isArray(previousRouteLines)) {
throw new Error("Hermes integration journal lacks owned routing block");
}
if (!text.includes(routeBlock)) throw new Error("Hermes routing block changed after enable; refusing destructive disable");
text = text.replace(routeBlock, previousRouteLines.filter((line): line is string => typeof line === "string").join("\n"));
for (const key of ["plugin_block", "mcp_block"] as const) {
const block = operation.owned?.[key];
if (block === null || block === undefined) continue;
if (typeof block !== "string" || !text.includes(block)) throw new Error(`Hermes ${key.replace("_block", "")} block changed after enable; refusing destructive disable`);
text = text.replace(block, "");
}
return Buffer.from(text);
}
const rootBlock = operation.owned?.root_block;
const tablesBlock = operation.owned?.tables_block;
if (typeof rootBlock !== "string" || typeof tablesBlock !== "string") throw new Error("Codex integration journal lacks owned blocks");
let text = current.toString("utf8");
for (const [block, begin] of [[rootBlock, CODEX_NATIVE_ROOT_BEGIN], [tablesBlock, CODEX_NATIVE_TABLES_BEGIN]] as const) {
if (text.includes(begin) && !text.includes(block)) throw new Error("Codex Caveman config block changed after enable; refusing destructive disable");
text = text.replace(`${block}\n\n`, "").replace(`\n\n${block}\n`, "\n").replace(block, "");
}
return Buffer.from(text);
}
function writeNativeRestoration(file: string, bytes: Buffer | null): void {
if (bytes) {
atomicWriteFile(file, bytes);View on GitHub (pinned to 3ee70a1026)