{"record":{"id":"9032d423e3a16101","repo":"JuliusBrussee/caveman","slug":"operation-file-changed-after-enable-refusing-destructive","errorCode":null,"errorMessage":"${operation.file} changed after enable; refusing destructive disable","messagePattern":"(.+?) changed after enable; refusing destructive disable","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":8348,"sourceCode":"    if (servers.caveman !== undefined) {\n      if (beforeServers.caveman === undefined) delete servers.caveman;\n      else servers.caveman = beforeServers.caveman;\n    }\n    if (Object.keys(servers).length > 0) currentRoot.mcpServers = servers;\n    else delete currentRoot.mcpServers;\n    return jsonBytes(currentRoot);\n  }\n  if (operation.kind === \"gemini-env\") {\n    const block = operation.owned?.route_block;\n    if (typeof block !== \"string\") throw new Error(\"Gemini integration journal lacks owned routing block\");\n    const text = current.toString(\"utf8\");\n    if (text.includes(GEMINI_NATIVE_ENV_BEGIN) && !text.includes(block)) {\n      throw new Error(\"Gemini routing block changed after enable; refusing destructive disable\");\n    }\n    return Buffer.from(text.replace(`${block}\\n\\n`, \"\").replace(`\\n\\n${block}\\n`, \"\\n\").replace(`${block}\\n`, \"\").replace(block, \"\"));\n  }\n  if (operation.kind === \"opencode-plugin\" || operation.kind === \"pi-extension\") {\n    throw new Error(`${operation.file} changed after enable; refusing destructive disable`);\n  }\n  if (operation.kind === \"opencode-config\") {\n    const root = parseJsonFileObject(operation.file, current);\n    const providers = root.provider && typeof root.provider === \"object\" && !Array.isArray(root.provider) ? root.provider as Record<string, unknown> : {};\n    const routes = operation.owned?.routes && typeof operation.owned.routes === \"object\" && !Array.isArray(operation.owned.routes) ? operation.owned.routes as Record<string, unknown> : {};\n    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> : {};\n    for (const providerID of [\"openai\", \"anthropic\"]) {\n      const provider = providers[providerID] && typeof providers[providerID] === \"object\" && !Array.isArray(providers[providerID]) ? providers[providerID] as Record<string, unknown> : {};\n      const options = provider.options && typeof provider.options === \"object\" && !Array.isArray(provider.options) ? provider.options as Record<string, unknown> : {};\n      if (options.baseURL !== undefined && options.baseURL !== routes[providerID]) {\n        throw new Error(`OpenCode ${providerID} baseURL changed after enable; refusing destructive disable`);\n      }\n      if (previousRoutes[providerID] === null || previousRoutes[providerID] === undefined) delete options.baseURL;\n      else options.baseURL = previousRoutes[providerID];\n      if (Object.keys(options).length > 0) provider.options = options;\n      else delete provider.options;\n      if (Object.keys(provider).length > 0) providers[providerID] = provider;\n      else delete providers[providerID];","sourceCodeStart":8330,"sourceCodeEnd":8366,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L8330-L8366","documentation":"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.","triggerScenarios":"`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).","commonSituations":"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.","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."],"exampleFix":"// verify drift\nshasum -a 256 ~/.config/opencode/plugins/caveman.js\n// compare to journal after_sha256; restore the recorded file, then:\ncaveman disable opencode","handlingStrategy":"validation","validationCode":"import { createHash, readFileSync } from \"node:crypto\";\nconst op = journal.operations.find(o => o.kind === \"opencode-plugin\" || o.kind === \"pi-extension\");\nconst hash = createHash(\"sha256\").update(readFileSync(op.file)).digest(\"hex\");\nif (hash !== op.after_sha256) {\n  console.error(`${op.file} changed after enable; revert or accept manual cleanup before disable`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  restoreNativeOperation(operation);\n} catch (err) {\n  if (String(err).includes(\"changed after enable; refusing destructive disable\")) {\n    // reconcile the file (restore recorded bytes or delete journal op) and retry\n  }\n}","preventionTips":["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."],"tags":["opencode","plugin","safety-guard","disable","hash-mismatch"],"backgroundTag":"invalid-state-transition","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"}