JuliusBrussee/caveman · error
Caveman hook changed after enable; refusing destructive…
Error message
${agent} ${event} Caveman hook changed after enable; refusing destructive disable What it means
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.
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.
Example fix
// before (settings.json, changed command)
{ "hooks": { "PostToolUse": [{ "command": "caveman hook --mode=fast" }] } }
// after (restore original recorded command)
{ "hooks": { "PostToolUse": [{ "command": "caveman hook" }] } } Defensive patterns
Strategy: validation
Validate before calling
import { readFileSync } from "node:fs";
const settings = JSON.parse(readFileSync("~/.claude/settings.json", "utf8"));
const journalAllowed = new Set(journal.allowedManaged);
for (const [event, list] of Object.entries(settings.hooks ?? {})) {
for (const entry of list) {
const cmd = entry.command ?? "";
if (cmd.includes("caveman") && !journalAllowed.has(cmd)) {
console.warn(`hook ${event} drifts from journal: ${cmd}`);
}
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Claude caveman MCP entry changed after enable; refusing…
- Gemini caveman MCP entry changed after enable; refusing…
- Gemini routing block changed after enable; refusing…
- Claude ANTHROPIC_BASE_URL changed after enable; refusing…
- changed after enable; refusing destructive disable
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/3f1a2c14a23a6f2c.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/index.ts:8230
const expected = nativeHooksDocument(agent, true).hooks as Record<string, unknown>;
const allowedManaged = new Set(
Object.values(expected)
.flatMap((raw) => Array.isArray(raw) ? raw as Array<Record<string, unknown>> : [])
.map(canonicalManagedHookEntry)
.filter(Boolean),
);
for (const event of Object.keys(hooks)) {
const expectedRaw = expected[event];
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(canonicalManagedHookEntry).filter(Boolean));
for (const entry of list) {
const command = hookEntryCommand(entry) ?? "";
const identity = managedHookIdentity(command);
const canonical = canonicalManagedHookEntry(entry);
const ownedNative = identity === `native-hook:${agent}`;
if (identity && !ownedNative && !allowedManaged.has(canonical)) {
throw new Error(`${agent} ${event} Caveman hook changed after enable; refusing destructive disable`);
}
}
const kept = list.filter((entry) => {
const identity = managedHookIdentity(hookEntryCommand(entry) ?? "");
if (!identity) return true;
if (identity === `native-hook:${agent}`) return false;
return !expectedStrings.has(canonicalManagedHookEntry(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");
}View on GitHub (pinned to 3ee70a1026)