affaan-m/ECC · error

Refusing to disable modified Claude hooks in

Error message

Refusing to disable modified Claude hooks in ${operation.destinationPath}; run the ECC uninstaller to review retained entries.

What it means

During a preflight of Claude settings operations, ECC found managed hooks in the destination settings file that no longer match what ECC originally wrote. Rather than silently disabling user-modified hooks, the installer aborts and directs the user to the ECC uninstaller, which offers an interactive review of each retained entry. This is a safety gate against destroying manual customizations.

Solutions

  1. Run the ECC uninstaller and review the retained hook entries, choosing which to keep or discard.
  2. Manually revert your edits to the managed hooks in the settings file so they match the original ECC output, then re-run the install.
  3. Remove the modified hook entries from the settings file yourself, then re-run the installer.
  4. Delete/repair the install-state record if it is stale, so previousManagedHooks match disk again.

Example fix

// before (hand-edited settings.json hook command)
"command": "node ~/.claude/scripts/hooks/my-custom-hook.js"
// after (restore ECC-managed hook or remove it, then rerun installer)
"command": "node ~/.claude/scripts/hooks/suggest-compact.js"
Defensive patterns

Strategy: validation

Validate before calling

import { findPreviousManagedHooks, readSettings } from './scripts/lib/install/apply.js';
const settings = readSettings(settingsPath);
const prev = findPreviousManagedHooks(state, plan, op);
if (prev.length !== settingsMatchingHooks(settings).length) {
  console.warn('Managed hooks were modified; run the ECC uninstaller first.');
}

Try / catch

try {
  await previewInstallPlan(plan);
} catch (e) {
  if (e.message.includes('Refusing to disable modified Claude hooks')) {
    await runUninstallerInteractive(); // review retained entries
  } else throw e;
}

Prevention

When it happens

Trigger: Calling preflightClaudeSettingsOperations (via previewInstallPlan or preparedClaudeSettings) with a 'remove-claude-settings-hooks' operation where uninstallManagedHooks(settings, managedHooks) returns a non-empty retained list — i.e. the hook entries on disk differ from the previously recorded managed hook state.

Common situations: A developer hand-edited ~/.claude/settings.json (changed the hook command, timeout, or matcher) after installing ECC, then re-runs the installer or an uninstall flow that needs to disable those hooks; or the install-state file is stale/missing so ECC cannot match the hooks it wrote.

Understand the failure class

Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/44e5e5b454e13484. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/install/apply.js:333

function preflightClaudeSettingsOperations(plan) {
  const settingsOperations = plan.operations.filter(operation => (
    operation.kind === 'update-claude-settings'
    || operation.kind === 'remove-claude-settings-hooks'
  ));
  if (settingsOperations.length === 0) {
    return new Map();
  }

  const previousState = readPreviousInstallState(plan);
  return new Map(settingsOperations.map(operation => {
    assertSafeInstallOperation(plan, operation);
    const managedHooks = validateManagedHooks(operation.managedHooks);
    const settings = readSettings(operation.destinationPath);
    const previousManagedHooks = findPreviousManagedHooks(previousState, plan, operation);
    if (operation.kind === 'remove-claude-settings-hooks') {
      const removal = uninstallManagedHooks(settings, managedHooks);
      if (removal.retained.length > 0) {
        throw new Error(
          `Refusing to disable modified Claude hooks in ${operation.destinationPath}; `
          + 'run the ECC uninstaller to review retained entries.'
        );
      }
    } else {
      mergeManagedHooks(settings, managedHooks, { previousManagedHooks });
    }
    return [operation, { managedHooks, previousManagedHooks }];
  }));
}

function prepareHookConsentMigration(plan, migration) {
  if (plan.hookConsent !== 'declined') {
    return migration;
  }
  const previousState = readPreviousInstallState(plan);
  if (!previousState) {
    return migration;

View on GitHub (pinned to 8321021c54)