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
- 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.
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
- 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
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
- Claude ANTHROPIC_BASE_URL changed after enable; refusing des
- Claude caveman MCP entry changed after enable; refusing dest
- Claude caveman-cloud MCP changed after setup; refusing to ov
- ${operation.file} was removed after enable; refusing destruc
- Gemini caveman MCP entry changed after enable; refusing dest
AI-assisted analysis of JuliusBrussee/caveman@2f49f0e1a3 (2026-08-18).
Data as JSON: /api/errors/3f1a2c14a23a6f2c.
Report an issue: GitHub.