{"record":{"id":"32b8adbca41bb494","repo":"affaan-m/ECC","slug":"refusing-to-manage-claude-hooks-outside-the-canonical","errorCode":null,"errorMessage":"Refusing to manage Claude hooks outside the canonical settings file: ${destinationPath}","messagePattern":"Refusing to manage Claude hooks outside the canonical settings file: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"scripts/lib/install/claude-settings.js","lineNumber":65,"sourceCode":"  return value;\n}\n\nfunction isNonEmptyString(value) {\n  return typeof value === 'string' && value.trim() !== '';\n}\n\nfunction getClaudeSettingsPath(targetRoot) {\n  return path.join(targetRoot, CLAUDE_SETTINGS_FILENAME);\n}\n\nfunction assertClaudeSettingsPath(destinationPath, trustedRoot) {\n  const resolvedDestination = path.resolve(destinationPath);\n  const resolvedExpected = path.resolve(getClaudeSettingsPath(trustedRoot));\n  const pathsMatch = process.platform === 'win32'\n    ? resolvedDestination.toLowerCase() === resolvedExpected.toLowerCase()\n    : resolvedDestination === resolvedExpected;\n  if (!pathsMatch) {\n    throw new Error(\n      `Refusing to manage Claude hooks outside the canonical settings file: ${destinationPath}`\n    );\n  }\n}\n\nfunction validateHookHandler(hook, label) {\n  if (!isJsonObject(hook)) {\n    throw new Error(`Invalid managed hook handler at ${label}: expected a JSON object`);\n  }\n  if (!VALID_HOOK_TYPES.has(hook.type)) {\n    throw new Error(`Invalid managed hook handler at ${label}: unsupported type`);\n  }\n  if (hook.timeout !== undefined && (typeof hook.timeout !== 'number' || hook.timeout < 0)) {\n    throw new Error(`Invalid managed hook handler at ${label}: invalid timeout`);\n  }\n\n  if (hook.type === 'command') {\n    const validCommand = isNonEmptyString(hook.command)","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/lib/install/claude-settings.js#L47-L83","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Pass the canonical ~/.claude/settings.json path (derived from the trusted root) instead of a project-local one.","If you intended project hooks, manage them outside ECC's managed-hooks mechanism.","In tests, configure the trusted root so the canonical path points at your temp location rather than passing an arbitrary destination."],"exampleFix":"// before\nawait manageHooks(path.join(repoRoot, '.claude', 'settings.json'))\n// after\nawait manageHooks(getClaudeSettingsPath(trustedRoot)) // ~/.claude/settings.json","handlingStrategy":"validation","validationCode":"import path from 'path';\nconst canonical = getClaudeSettingsPath(trustedRoot); // e.g. ~/.claude/settings.json\nif (path.resolve(destinationPath) !== path.resolve(canonical)) {\n  throw new Error(`Hook management only supports ${canonical}`);\n}","typeGuard":"const isCanonicalClaudeSettings = (p, trustedRoot) => path.resolve(p) === path.resolve(getClaudeSettingsPath(trustedRoot));","tryCatchPattern":"try {\n  await manageHooks(destinationPath);\n} catch (e) {\n  if (e.message.startsWith('Refusing to manage Claude hooks outside')) {\n    await manageHooks(getClaudeSettingsPath(trustedRoot)); // fall back to canonical\n  } else throw e;\n}","preventionTips":["Always derive the settings path from getClaudeSettingsPath(trustedRoot), never from cwd or repo root.","Remember project-local .claude/settings.json is NOT managed by ECC's hook mechanism.","In tests, override the trusted root instead of passing arbitrary destination paths."],"tags":["hooks","path-validation","settings","safety-gate"],"backgroundTag":"invalid-argument-value","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}