{"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":"Error","httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":7540,"sourceCode":"}\n\nfunction removeNativeHookEntries(root: Record<string, unknown>, agent: \"claude\" | \"codex\" | \"gemini\"): Record<string, unknown> {\n  const hooks = root.hooks && typeof root.hooks === \"object\" && !Array.isArray(root.hooks)\n    ? root.hooks as Record<string, unknown>\n    : undefined;\n  if (!hooks) return root;\n  const expected = nativeHooksDocument(agent, true).hooks as Record<string, unknown>;\n  for (const [event, expectedRaw] of Object.entries(expected)) {\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((entry) => JSON.stringify(entry)));\n    for (const entry of list) {\n      const encoded = JSON.stringify(entry);\n      const command = hookEntryCommand(entry) ?? \"\";\n      const nativeMarker = `native-hook ${agent}`;\n      const looksManagedNative = command.endsWith(nativeMarker) || command.includes(`${nativeMarker} --adapter `);\n      if ((looksManagedNative || /(?:^|\\s)shrink-hook$/.test(command)) && !expectedStrings.has(encoded)) {\n        throw new Error(`${agent} ${event} Caveman hook changed after enable; refusing destructive disable`);\n      }\n    }\n    const kept = list.filter((entry) => !expectedStrings.has(JSON.stringify(entry)));\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}\n\nfunction restoreNativeOperation(operation: NativeJournal[\"operations\"][number]): Buffer | null {\n  const current = fileBytes(operation.file);\n  const before = nativeBackupBytes(operation);\n  if (!current) {","sourceCodeStart":7522,"sourceCodeEnd":7558,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/2f49f0e1a352aa810e70056b7930aeb0b3d219b4/packages/cli/src/index.ts#L7522-L7558","documentation":"removeNativeHookEntries() deletes only hook entries that JSON-stringify exactly to the expected caveman hooks for that agent and event. An entry whose command looks managed (ends with 'native-hook <agent>', contains 'native-hook <agent> --adapter ', or ends with 'shrink-hook') but differs from the expected form is treated as user-modified, and disable aborts rather than delete your customized hook.","triggerScenarios":"`caveman disable claude|codex|gemini` after you edited the caveman hook entry in the agent's settings JSON - changed the command path, wrapped it in a script, added args or env - so it no longer byte-matches the journalled entry for that hook event.","commonSituations":"Pointing the hook at a wrapper for logging; adding environment variables; another tool reformatting/reordering keys in settings.json so the JSON string changes even though semantics did not.","solutions":["If the edit was intentional, delete that hook entry yourself in the settings file, then re-run `caveman disable <agent>`.","Or revert the entry to the exact expected shape: `caveman doctor <agent> --fix` rewrites managed hooks to canonical form first, then disable proceeds.","Keep custom hooks under different event entries or commands that do not end with the caveman markers."],"exampleFix":"// before ~/.claude/settings.json - user-modified caveman hook\n\"SessionStart\": [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"caveman native-hook claude --adapter my-wrap\" }] }]\n\n// after - remove the entry by hand (or `caveman doctor claude --fix`), then\n$ caveman disable claude","handlingStrategy":"validation","validationCode":"import { spawnSync } from 'node:child_process';\nconst agent = 'claude'; // codex | gemini\nconst r = spawnSync('caveman', ['doctor', agent], { encoding: 'utf8' });\n// degraded 'owned' checks surface hook drift before disable refuses\nif (r.status !== 0) { console.error(`run 'caveman doctor ${agent} --fix' before disable`); process.exit(1); }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Do not edit caveman hook entries; wrap at a different layer or add separate hooks","Before disable, run `caveman doctor <agent> --fix` to canonicalize managed hooks","Beware tools that reformat settings JSON - key order changes break byte-matching"],"tags":["hooks","disable","config-drift","claude"],"backgroundTag":"config-drift-refusal","analyzedSha":"2f49f0e1a352aa810e70056b7930aeb0b3d219b4","analyzedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}