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
- 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.
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
- 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.
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
- Refusing to disable modified Claude hooks in
- Claude settings contains multiple hooks for event
- Invalid : expected hooks. to be an array
- Invalid : expected "hooks" to be a JSON object
- Refusing to overwrite Claude hook for event
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)