affaan-m/ECC · error

Claude settings contains multiple hooks for event

Error message

Claude settings contains multiple hooks for event "${event}" and id "${id}"

What it means

assertUnambiguousMatch finds hook entries matching a given event and managed id, and throws if more than one entry in the Claude settings contains that id, because removal or replacement would be ambiguous. Exactly zero or one entry is required; the single match (or null) is returned.

Solutions

  1. Open settings.json and remove the duplicate entry so the id appears at most once under the event.
  2. Keep only the entry whose contents match the current ECC-managed hook definition.
  3. If unsure which duplicate is correct, back up settings.json and let the installer re-add the managed hook cleanly.
  4. Rerun the install/repair after de-duplicating.

Example fix

// before (settings.json)
"PostToolUse": [ {"id": "fmt"}, {"id": "fmt"} ]
// after
"PostToolUse": [ {"id": "fmt"} ]
Defensive patterns

Strategy: validation

Validate before calling

function hasDuplicateHookIds(settings, event, id) {
  const entries = (settings.hooks?.[event] ?? []).filter(e => e && e.id === id);
  return entries.length > 1;
}

Try / catch

try {
  await uninstallManagedHooks(path, event, id);
} catch (error) {
  if (error.message.includes('multiple hooks for event')) {
    console.error('De-duplicate the hook id in settings.json, then retry');
    return;
  }
  throw error;
}

Prevention

When it happens

Trigger: Calling uninstallManagedHooks or match for event/id when settings.json manually contains the same hook id twice under the same event array (e.g. duplicated by a hand-edit or a copy-paste of a hook block).

Common situations: A user duplicated a managed hook entry while editing settings.json, restored an old file over a newer one producing duplicates, or merged settings from two machines concatenating hook arrays.

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


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

Appendix: source

Thrown at scripts/lib/install/claude-settings.js:462

    return update();
  }
  return runWithSettingsLock(settingsPath, update);
}

function reference(event, id) {
  return { event, id };
}

function entriesMatchingId(entries, id) {
  return entries
    .map((entry, index) => ({ entry, index }))
    .filter(candidate => isJsonObject(candidate.entry) && candidate.entry.id === id);
}

function assertUnambiguousMatch(entries, event, id) {
  const matches = entriesMatchingId(entries, id);
  if (matches.length > 1) {
    throw new Error(
      `Claude settings contains multiple hooks for event "${event}" and id "${id}"`
    );
  }
  return matches[0] || null;
}

function managedEntryFor(managedHooks, event, id) {
  const entries = managedHooks && managedHooks[event];
  if (!Array.isArray(entries)) {
    return null;
  }
  return entries.find(entry => entry.id === id) || null;
}

function mergeManagedHooks(settings, managedHooks, options = {}) {
  const validatedSettings = validateSettings(settings);
  const desiredHooks = validateManagedHooks(managedHooks);
  const previousHooks = options.previousManagedHooks === undefined

View on GitHub (pinned to 8321021c54)