JuliusBrussee/caveman · error · Error

${agent} ${event} Caveman hook changed after enable; refusin

Error message

${agent} ${event} Caveman hook changed after enable; refusing destructive disable

What it means

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.

Source

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

}

function removeNativeHookEntries(root: Record<string, unknown>, agent: "claude" | "codex" | "gemini"): Record<string, unknown> {
  const hooks = root.hooks && typeof root.hooks === "object" && !Array.isArray(root.hooks)
    ? root.hooks as Record<string, unknown>
    : undefined;
  if (!hooks) return root;
  const expected = nativeHooksDocument(agent, true).hooks as Record<string, unknown>;
  for (const [event, expectedRaw] of Object.entries(expected)) {
    const list = Array.isArray(hooks[event]) ? hooks[event] as Array<Record<string, unknown>> : [];
    const expectedEntries = Array.isArray(expectedRaw) ? expectedRaw as Array<Record<string, unknown>> : [];
    const expectedStrings = new Set(expectedEntries.map((entry) => JSON.stringify(entry)));
    for (const entry of list) {
      const encoded = JSON.stringify(entry);
      const command = hookEntryCommand(entry) ?? "";
      const nativeMarker = `native-hook ${agent}`;
      const looksManagedNative = command.endsWith(nativeMarker) || command.includes(`${nativeMarker} --adapter `);
      if ((looksManagedNative || /(?:^|\s)shrink-hook$/.test(command)) && !expectedStrings.has(encoded)) {
        throw new Error(`${agent} ${event} Caveman hook changed after enable; refusing destructive disable`);
      }
    }
    const kept = list.filter((entry) => !expectedStrings.has(JSON.stringify(entry)));
    if (kept.length > 0) hooks[event] = kept;
    else delete hooks[event];
  }
  if (Object.keys(hooks).length === 0) delete root.hooks;
  return root;
}

function jsonBytes(root: Record<string, unknown>): Buffer {
  return Buffer.from(JSON.stringify(root, null, 2) + "\n");
}

function restoreNativeOperation(operation: NativeJournal["operations"][number]): Buffer | null {
  const current = fileBytes(operation.file);
  const before = nativeBackupBytes(operation);
  if (!current) {

View on GitHub (pinned to 2f49f0e1a3)

Solutions

  1. If the edit was intentional, delete that hook entry yourself in the settings file, then re-run `caveman disable <agent>`.
  2. Or revert the entry to the exact expected shape: `caveman doctor <agent> --fix` rewrites managed hooks to canonical form first, then disable proceeds.
  3. Keep custom hooks under different event entries or commands that do not end with the caveman markers.

Example fix

// before ~/.claude/settings.json - user-modified caveman hook
"SessionStart": [{ "hooks": [{ "type": "command", "command": "caveman native-hook claude --adapter my-wrap" }] }]

// after - remove the entry by hand (or `caveman doctor claude --fix`), then
$ caveman disable claude
Defensive patterns

Strategy: validation

Validate before calling

import { spawnSync } from 'node:child_process';
const agent = 'claude'; // codex | gemini
const r = spawnSync('caveman', ['doctor', agent], { encoding: 'utf8' });
// degraded 'owned' checks surface hook drift before disable refuses
if (r.status !== 0) { console.error(`run 'caveman doctor ${agent} --fix' before disable`); process.exit(1); }

Prevention

When it happens

Trigger: `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.

Common situations: 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.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@2f49f0e1a3 (2026-08-18). Data as JSON: /api/errors/3f1a2c14a23a6f2c. Report an issue: GitHub.