affaan-m/ECC · error
Refusing to remove Claude hook for event
Error message
Refusing to remove Claude hook for event "${event}" and id "${previousEntry.id}" because the previous managed entry has drifted What it means
During mergeManagedHooks removal of stale managed hooks, if a previously recorded managed hook entry exists in settings but its contents no longer deep-equal the recorded previous entry, the merge refuses to remove it. This protects hand-modified or externally changed hooks from being silently deleted. Repair mode and exact matches proceed with removal.
Solutions
- Revert your manual edits to that hook entry so it matches the previously installed version, then rerun.
- Run the operation in repair mode (mode: 'repair' or repair: true) if you intentionally want the drifted hook replaced/removed.
- Manually delete the drifted hook from settings.json if you no longer want it, then rerun the merge.
- Back up settings.json before using repair mode so local customizations can be recovered.
Example fix
// before
await mergeManagedHooks({ settings, desiredHooks, previousManagedHooks });
// after (explicitly allow replacing drifted entries)
await mergeManagedHooks({ settings, desiredHooks, previousManagedHooks, mode: 'repair' }); Defensive patterns
Strategy: try-catch
Try / catch
try {
await mergeManagedHooks({ settings, desiredHooks, previousManagedHooks });
} catch (error) {
if (error.message.includes('previous managed entry has drifted')) {
// ask user or run repair deliberately
await mergeManagedHooks({ settings, desiredHooks, previousManagedHooks, mode: 'repair' });
} else throw error;
} Prevention
- Don't hand-edit ECC-managed hook entries; keep custom hooks under distinct ids
- Back up settings.json before reinstall/update operations
- Sync the recorded previousManagedHooks state together with settings.json
- Use repair mode explicitly when you intend to replace local modifications
When it happens
Trigger: Running an uninstall/merge where previousManagedHooks records a hook that the user (or another tool) has since edited in settings.json; the recorded id still matches but the entry object has drifted.
Common situations: A user customized command strings, matchers, or timeouts of an ECC-managed hook in settings.json, then ran the installer which wanted to remove or replace the stale hook.
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
- Refusing to overwrite Claude hook for event
- Claude settings contains multiple hooks for event
- Invalid : expected hooks. to be an array
- Invalid : expected "hooks" to be a JSON object
- Refusing to disable modified Claude hooks in
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/0ea627e1c111d566.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/lib/install/claude-settings.js:505
}
let nextHooks = validatedSettings.hooks
? cloneValue(validatedSettings.hooks)
: {};
const added = [];
const updated = [];
const unchanged = [];
const removed = [];
if (previousHooks) {
for (const [event, previousEntries] of Object.entries(previousHooks)) {
let eventEntries = nextHooks[event] ? cloneValue(nextHooks[event]) : [];
for (const previousEntry of previousEntries) {
if (managedEntryFor(desiredHooks, event, previousEntry.id)) continue;
const match = assertUnambiguousMatch(eventEntries, event, previousEntry.id);
if (!match) continue;
if (!isDeepStrictEqual(match.entry, previousEntry)) {
throw new Error(
`Refusing to remove Claude hook for event "${event}" and id `
+ `"${previousEntry.id}" because the previous managed entry has drifted`
);
}
eventEntries = eventEntries.filter((_entry, index) => index !== match.index);
removed.push(reference(event, previousEntry.id));
}
nextHooks = eventEntries.length > 0
? { ...nextHooks, [event]: eventEntries }
: withoutProperty(nextHooks, event);
}
}
for (const [event, desiredEntries] of Object.entries(desiredHooks)) {
let eventEntries = nextHooks[event] ? cloneValue(nextHooks[event]) : [];
for (const desiredEntry of desiredEntries) {
const match = assertUnambiguousMatch(eventEntries, event, desiredEntry.id);
if (!match) {View on GitHub (pinned to 8321021c54)