affaan-m/ECC · error

Refusing to manage Claude hooks outside the canonical…

Error message

Refusing to manage Claude hooks outside the canonical settings file: ${destinationPath}

What it means

assertClaudeSettingsPath resolves the given destination and compares it against the canonical Claude settings path derived from the trusted root (case-insensitively on Windows). Hook management is only allowed on that exact canonical file; any other path is rejected to prevent writing hook config into the wrong settings file.

Solutions

  1. Pass the canonical ~/.claude/settings.json path (derived from the trusted root) instead of a project-local one.
  2. If you intended project hooks, manage them outside ECC's managed-hooks mechanism.
  3. In tests, configure the trusted root so the canonical path points at your temp location rather than passing an arbitrary destination.

Example fix

// before
await manageHooks(path.join(repoRoot, '.claude', 'settings.json'))
// after
await manageHooks(getClaudeSettingsPath(trustedRoot)) // ~/.claude/settings.json
Defensive patterns

Strategy: validation

Validate before calling

import path from 'path';
const canonical = getClaudeSettingsPath(trustedRoot); // e.g. ~/.claude/settings.json
if (path.resolve(destinationPath) !== path.resolve(canonical)) {
  throw new Error(`Hook management only supports ${canonical}`);
}

Type guard

const isCanonicalClaudeSettings = (p, trustedRoot) => path.resolve(p) === path.resolve(getClaudeSettingsPath(trustedRoot));

Try / catch

try {
  await manageHooks(destinationPath);
} catch (e) {
  if (e.message.startsWith('Refusing to manage Claude hooks outside')) {
    await manageHooks(getClaudeSettingsPath(trustedRoot)); // fall back to canonical
  } else throw e;
}

Prevention

When it happens

Trigger: Calling hook-management APIs (via assertClaudeSettingsDestination) with a destinationPath that resolves to anything other than <trustedRoot>/.claude/settings.json — e.g. project-local settings, a copy of the file, or a symlink resolving elsewhere.

Common situations: Pointing tooling at a repo-local .claude/settings.json expecting hooks to be managed there; passing a relative path that resolves differently than expected; testing with a temp settings file that isn't the canonical path.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

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

  return value;
}

function isNonEmptyString(value) {
  return typeof value === 'string' && value.trim() !== '';
}

function getClaudeSettingsPath(targetRoot) {
  return path.join(targetRoot, CLAUDE_SETTINGS_FILENAME);
}

function assertClaudeSettingsPath(destinationPath, trustedRoot) {
  const resolvedDestination = path.resolve(destinationPath);
  const resolvedExpected = path.resolve(getClaudeSettingsPath(trustedRoot));
  const pathsMatch = process.platform === 'win32'
    ? resolvedDestination.toLowerCase() === resolvedExpected.toLowerCase()
    : resolvedDestination === resolvedExpected;
  if (!pathsMatch) {
    throw new Error(
      `Refusing to manage Claude hooks outside the canonical settings file: ${destinationPath}`
    );
  }
}

function validateHookHandler(hook, label) {
  if (!isJsonObject(hook)) {
    throw new Error(`Invalid managed hook handler at ${label}: expected a JSON object`);
  }
  if (!VALID_HOOK_TYPES.has(hook.type)) {
    throw new Error(`Invalid managed hook handler at ${label}: unsupported type`);
  }
  if (hook.timeout !== undefined && (typeof hook.timeout !== 'number' || hook.timeout < 0)) {
    throw new Error(`Invalid managed hook handler at ${label}: invalid timeout`);
  }

  if (hook.type === 'command') {
    const validCommand = isNonEmptyString(hook.command)

View on GitHub (pinned to 8321021c54)