#!/usr/bin/env node /** * Shared hook enable/disable controls. * * Controls: * - ECC_HOOKS_ENABLED=true|false (default: true) * - ECC_HOOK_PROFILE=minimal|standard|strict (default: standard) * - ECC_DISABLED_HOOKS=comma,separated,hook,ids * * Claude plugin options are used when their corresponding ECC variable is * absent. A managed install can provide ecc/setup.json as the final fallback. */ 'use strict'; const fs = require('fs'); const path = require('path'); const VALID_PROFILES = new Set(['minimal', 'standard', 'strict']); function normalizeId(value) { return String(value || '').trim().toLowerCase(); } function parseBoolean(value, fallback = true) { if (value === undefined || value === null || String(value).trim() === '') { return fallback; } const normalized = String(value).trim().toLowerCase(); if (['1', 'true', 'yes', 'on'].includes(normalized)) return true; if (['0', 'false', 'no', 'off'].includes(normalized)) return false; return fallback; } function sanitizeDiagnostic(value) { return String(value || '') // eslint-disable-next-line no-control-regex .replace(/\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|\([A-Z]|[A-Z])/g, '') .replace(/[^\x20-\x7E]/g, '?'); } function readManagedHookConfig(env = process.env) { const pluginRoot = String( env.CLAUDE_PLUGIN_ROOT || env.ECC_PLUGIN_ROOT || '' ).trim(); const configPath = String(env.ECC_HOOK_CONFIG || '').trim() || (pluginRoot ? path.join(pluginRoot, 'ecc', 'setup.json') : ''); if (!configPath || !fs.existsSync(configPath)) return {}; try { const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); return config?.hooks && typeof config.hooks === 'object' && !Array.isArray(config.hooks) ? config.hooks : {}; } catch (error) { process.stderr.write(`${sanitizeDiagnostic( `Warning: unable to read managed ECC hook config at ${configPath}: ${error.message}` )}\n`); return {}; } } function areHooksEnabled(env = process.env, managed = readManagedHookConfig(env)) { const raw = env.ECC_HOOKS_ENABLED !== undefined ? env.ECC_HOOKS_ENABLED : ( env.CLAUDE_PLUGIN_OPTION_HOOKS_ENABLED !== undefined ? env.CLAUDE_PLUGIN_OPTION_HOOKS_ENABLED : managed.enabled ); return parseBoolean(raw, true); } function getHookProfile(env = process.env, managed = readManagedHookConfig(env)) { const selected = env.ECC_HOOK_PROFILE !== undefined ? env.ECC_HOOK_PROFILE : ( env.CLAUDE_PLUGIN_OPTION_HOOK_PROFILE !== undefined ? env.CLAUDE_PLUGIN_OPTION_HOOK_PROFILE : managed.profile ); const raw = String(selected ?? 'standard').trim().toLowerCase(); return VALID_PROFILES.has(raw) ? raw : 'standard'; } function getDisabledHookIds(env = process.env) { const raw = String(env.ECC_DISABLED_HOOKS || ''); if (!raw.trim()) return new Set(); return new Set( raw .split(',') .map(v => normalizeId(v)) .filter(Boolean) ); } function parseProfiles(rawProfiles, fallback = ['standard', 'strict']) { if (!rawProfiles) return [...fallback]; if (Array.isArray(rawProfiles)) { const parsed = rawProfiles .map(v => String(v || '').trim().toLowerCase()) .filter(v => VALID_PROFILES.has(v)); return parsed.length > 0 ? parsed : [...fallback]; } const parsed = String(rawProfiles) .split(',') .map(v => v.trim().toLowerCase()) .filter(v => VALID_PROFILES.has(v)); return parsed.length > 0 ? parsed : [...fallback]; } function isDryRun(env = process.env) { return env.ECC_DRY_RUN === '1'; } function isHookEnabled(hookId, options = {}) { const env = options.env || process.env; const managed = readManagedHookConfig(env); if (!areHooksEnabled(env, managed)) { return false; } const id = normalizeId(hookId); if (!id) return true; const disabled = getDisabledHookIds(env); if (disabled.has(id)) { return false; } const profile = getHookProfile(env, managed); const allowedProfiles = parseProfiles(options.profiles); return allowedProfiles.includes(profile); } module.exports = { VALID_PROFILES, normalizeId, parseBoolean, readManagedHookConfig, areHooksEnabled, getHookProfile, getDisabledHookIds, parseProfiles, isHookEnabled, isDryRun, };