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
- Revert the plugin/extension file to its post-enable content (hash must equal `operation.after_sha256` in the journal), then retry disable.
- Delete the plugin file reference and the journal operation manually if you intend to keep your modified version of the file.
- 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).
- 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
- Don't edit plugin/extension files while a caveman journal claims ownership.
- Disable before upgrading or replacing the caveman plugin file.
- Verify file hashes against the journal (`after_sha256`) before disabling.
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
- Caveman hook changed after enable; refusing destructive…
- Claude ANTHROPIC_BASE_URL changed after enable; refusing…
- Claude caveman MCP entry changed after enable; refusing…
- Gemini caveman MCP entry changed after enable; refusing…
- Gemini routing block changed after enable; refusing…
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)