{"record":{"id":"3f1a2c14a23a6f2c","repo":"JuliusBrussee/caveman","slug":"agent-event-caveman-hook-changed-after-enabl","errorCode":null,"errorMessage":"${agent} ${event} Caveman hook changed after enable; refusing destructive disable","messagePattern":"(.+?) (.+?) Caveman hook changed after enable; refusing destructive disable","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":8230,"sourceCode":"  const expected = nativeHooksDocument(agent, true).hooks as Record<string, unknown>;\n  const allowedManaged = new Set(\n    Object.values(expected)\n      .flatMap((raw) => Array.isArray(raw) ? raw as Array<Record<string, unknown>> : [])\n      .map(canonicalManagedHookEntry)\n      .filter(Boolean),\n  );\n  for (const event of Object.keys(hooks)) {\n    const expectedRaw = expected[event];\n    const list = Array.isArray(hooks[event]) ? hooks[event] as Array<Record<string, unknown>> : [];\n    const expectedEntries = Array.isArray(expectedRaw) ? expectedRaw as Array<Record<string, unknown>> : [];\n    const expectedStrings = new Set(expectedEntries.map(canonicalManagedHookEntry).filter(Boolean));\n    for (const entry of list) {\n      const command = hookEntryCommand(entry) ?? \"\";\n      const identity = managedHookIdentity(command);\n      const canonical = canonicalManagedHookEntry(entry);\n      const ownedNative = identity === `native-hook:${agent}`;\n      if (identity && !ownedNative && !allowedManaged.has(canonical)) {\n        throw new Error(`${agent} ${event} Caveman hook changed after enable; refusing destructive disable`);\n      }\n    }\n    const kept = list.filter((entry) => {\n      const identity = managedHookIdentity(hookEntryCommand(entry) ?? \"\");\n      if (!identity) return true;\n      if (identity === `native-hook:${agent}`) return false;\n      return !expectedStrings.has(canonicalManagedHookEntry(entry));\n    });\n    if (kept.length > 0) hooks[event] = kept;\n    else delete hooks[event];\n  }\n  if (Object.keys(hooks).length === 0) delete root.hooks;\n  return root;\n}\n\nfunction jsonBytes(root: Record<string, unknown>): Buffer {\n  return Buffer.from(JSON.stringify(root, null, 2) + \"\\n\");\n}","sourceCodeStart":8212,"sourceCodeEnd":8248,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L8212-L8248","documentation":"During a destructive disable of a native integration, the CLI walks the agent's hooks for each event and verifies every Caveman-managed hook entry still matches what enable recorded (`allowedManaged`) or is the agent's native hook (`native-hook:<agent>`). If a managed hook command was changed after enable, the CLI cannot safely decide what to remove, so it refuses the destructive disable rather than deleting a user-modified hook.","triggerScenarios":"Disable runs `restoreNativeOperation` hook cleanup when an entry's `managedHookIdentity(hookEntryCommand(entry))` is non-null, not `native-hook:${agent}`, and its `canonicalManagedHookEntry` is not in the `allowedManaged` set captured at enable time.","commonSituations":"The user hand-edited a caveman hook command in Claude/Codex/Gemini settings; a caveman upgrade changed the hook command so it no longer matches the enabled journal; a wrapper script wraps the hook differently.","solutions":["Restore the hook entry to the exact form recorded at enable time (check the journal's `allowedManaged` entries), then retry disable.","Re-run `caveman doctor <agent> --fix` to reconcile the hook entries, then disable.","Manually remove the changed hook entry from the agent's settings file and re-run disable.","If the modification is intentional, delete the corresponding journal/operation so disable no longer treats it as owned."],"exampleFix":"// before (settings.json, changed command)\n{ \"hooks\": { \"PostToolUse\": [{ \"command\": \"caveman hook --mode=fast\" }] } }\n\n// after (restore original recorded command)\n{ \"hooks\": { \"PostToolUse\": [{ \"command\": \"caveman hook\" }] } }","handlingStrategy":"validation","validationCode":"import { readFileSync } from \"node:fs\";\nconst settings = JSON.parse(readFileSync(\"~/.claude/settings.json\", \"utf8\"));\nconst journalAllowed = new Set(journal.allowedManaged);\nfor (const [event, list] of Object.entries(settings.hooks ?? {})) {\n  for (const entry of list) {\n    const cmd = entry.command ?? \"\";\n    if (cmd.includes(\"caveman\") && !journalAllowed.has(cmd)) {\n      console.warn(`hook ${event} drifts from journal: ${cmd}`);\n    }\n  }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Do not modify caveman hook commands in agent settings; re-run enable to change them.","Diff agent settings against the journal before disabling.","Run `doctor --fix` instead of hand-editing when a hook looks stale."],"tags":["hooks","safety-guard","disable","config-drift"],"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"}