JuliusBrussee/caveman · error

changed after enable; refusing destructive disable

Error message

${operation.file} changed after enable; refusing destructive disable

What it means

For `opencode-plugin` and `pi-extension` journal operations, the CLI has no structured before/after representation of the plugin/extension file, so it can only restore by assuming the file is byte-identical to what enable recorded (matched via `after_sha256` earlier in `restoreNativeOperation`). If the file hash differs, the file changed after enable and the CLI refuses the destructive disable because it cannot safely undo arbitrary plugin-file changes.

Solutions

  1. Revert the plugin/extension file to its post-enable content (hash must equal `operation.after_sha256` in the journal), then retry disable.
  2. Delete the plugin file reference and the journal operation manually if you intend to keep your modified version of the file.
  3. Reinstall the exact plugin file version caveman wrote at enable (re-run enable with the same caveman version to regenerate it, or restore from the journal's backup bytes).
  4. Remove the stale journal and run `caveman doctor opencode --fix` to rebuild a consistent state.

Example fix

// verify drift
shasum -a 256 ~/.config/opencode/plugins/caveman.js
// compare to journal after_sha256; restore the recorded file, then:
caveman disable opencode
Defensive patterns

Strategy: validation

Validate before calling

import { createHash, readFileSync } from "node:crypto";
const op = journal.operations.find(o => o.kind === "opencode-plugin" || o.kind === "pi-extension");
const hash = createHash("sha256").update(readFileSync(op.file)).digest("hex");
if (hash !== op.after_sha256) {
  console.error(`${op.file} changed after enable; revert or accept manual cleanup before disable`);
}

Try / catch

try {
  restoreNativeOperation(operation);
} catch (err) {
  if (String(err).includes("changed after enable; refusing destructive disable")) {
    // reconcile the file (restore recorded bytes or delete journal op) and retry
  }
}

Prevention

When it happens

Trigger: `restoreNativeOperation` where `operation.kind === 'opencode-plugin' || operation.kind === 'pi-extension'` and the current file bytes' hash does not equal `operation.after_sha256` (i.e. the file was modified after enable).

Common situations: The user edited the opencode plugin or pi extension file after enabling caveman; a plugin manager updated/reformatted the file; a caveman upgrade replaced the plugin with a new version while the old journal remained.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/9032d423e3a16101. Report an issue: GitHub.

Appendix: source

Thrown at packages/cli/src/index.ts:8348

    if (servers.caveman !== undefined) {
      if (beforeServers.caveman === undefined) delete servers.caveman;
      else servers.caveman = beforeServers.caveman;
    }
    if (Object.keys(servers).length > 0) currentRoot.mcpServers = servers;
    else delete currentRoot.mcpServers;
    return jsonBytes(currentRoot);
  }
  if (operation.kind === "gemini-env") {
    const block = operation.owned?.route_block;
    if (typeof block !== "string") throw new Error("Gemini integration journal lacks owned routing block");
    const text = current.toString("utf8");
    if (text.includes(GEMINI_NATIVE_ENV_BEGIN) && !text.includes(block)) {
      throw new Error("Gemini routing block changed after enable; refusing destructive disable");
    }
    return Buffer.from(text.replace(`${block}\n\n`, "").replace(`\n\n${block}\n`, "\n").replace(`${block}\n`, "").replace(block, ""));
  }
  if (operation.kind === "opencode-plugin" || operation.kind === "pi-extension") {
    throw new Error(`${operation.file} changed after enable; refusing destructive disable`);
  }
  if (operation.kind === "opencode-config") {
    const root = parseJsonFileObject(operation.file, current);
    const providers = root.provider && typeof root.provider === "object" && !Array.isArray(root.provider) ? root.provider as Record<string, unknown> : {};
    const routes = operation.owned?.routes && typeof operation.owned.routes === "object" && !Array.isArray(operation.owned.routes) ? operation.owned.routes as Record<string, unknown> : {};
    const previousRoutes = operation.owned?.previous_routes && typeof operation.owned.previous_routes === "object" && !Array.isArray(operation.owned.previous_routes) ? operation.owned.previous_routes as Record<string, unknown> : {};
    for (const providerID of ["openai", "anthropic"]) {
      const provider = providers[providerID] && typeof providers[providerID] === "object" && !Array.isArray(providers[providerID]) ? providers[providerID] as Record<string, unknown> : {};
      const options = provider.options && typeof provider.options === "object" && !Array.isArray(provider.options) ? provider.options as Record<string, unknown> : {};
      if (options.baseURL !== undefined && options.baseURL !== routes[providerID]) {
        throw new Error(`OpenCode ${providerID} baseURL changed after enable; refusing destructive disable`);
      }
      if (previousRoutes[providerID] === null || previousRoutes[providerID] === undefined) delete options.baseURL;
      else options.baseURL = previousRoutes[providerID];
      if (Object.keys(options).length > 0) provider.options = options;
      else delete provider.options;
      if (Object.keys(provider).length > 0) providers[providerID] = provider;
      else delete providers[providerID];

View on GitHub (pinned to 3ee70a1026)