import { resetSdkInitState } from '../../bootstrap/state.js' import { isRestrictedToPluginOnly } from '../settings/pluginOnlyPolicy.js' // Import as module object so spyOn works in tests (direct imports bypass spies) import * as settingsModule from '../settings/settings.js' import { resetSettingsCache } from '../settings/settingsCache.js' import type { HooksSettings } from '../settings/types.js' let initialHooksConfig: HooksSettings | null = null /** * Get hooks from allowed sources. * If allowManagedHooksOnly is set in policySettings, only managed hooks are returned. * If disableAllHooks is set in policySettings, no hooks are returned. * If disableAllHooks is set in non-managed settings, only managed hooks are returned * (non-managed settings cannot disable managed hooks). * Otherwise, returns merged hooks from all sources (backwards compatible). */ function getHooksFromAllowedSources(): HooksSettings { const policySettings = settingsModule.getSettingsForSource('policySettings') // If managed settings disables all hooks, return empty if (policySettings?.disableAllHooks === true) { return {} } // If allowManagedHooksOnly is set in managed settings, only use managed hooks if (policySettings?.allowManagedHooksOnly === true) { return policySettings.hooks ?? {} } // strictPluginOnlyCustomization: block user/project/local settings hooks. // Plugin hooks (registered channel, hooks.ts:1391) are NOT affected — // they're assembled separately and the managedOnly skip there is keyed // on shouldAllowManagedHooksOnly(), not on this policy. Agent frontmatter // hooks are gated at REGISTRATION (runAgent.ts:~535) by agent source — // plugin/built-in/policySettings agents register normally, user-sourced // agents skip registration under ["hooks"]. A blanket execution-time // block here would over-kill plugin agents' hooks. if (isRestrictedToPluginOnly('hooks')) { return policySettings?.hooks ?? {} } const mergedSettings = settingsModule.getSettings_DEPRECATED() // If disableAllHooks is set in non-managed settings, only managed hooks still run // (non-managed settings cannot override managed hooks) if (mergedSettings.disableAllHooks === true) { return policySettings?.hooks ?? {} } // Otherwise, use all hooks (merged from all sources) - backwards compatible return mergedSettings.hooks ?? {} } /** * Check if only managed hooks should run. * This is true when: * - policySettings has allowManagedHooksOnly: true, OR * - disableAllHooks is set in non-managed settings (non-managed settings * cannot disable managed hooks, so they effectively become managed-only) */ export function shouldAllowManagedHooksOnly(): boolean { const policySettings = settingsModule.getSettingsForSource('policySettings') if (policySettings?.allowManagedHooksOnly === true) { return true } // If disableAllHooks is set but NOT from managed settings, // treat as managed-only (non-managed hooks disabled, managed hooks still run) if ( settingsModule.getSettings_DEPRECATED().disableAllHooks === true && policySettings?.disableAllHooks !== true ) { return true } return false } /** * Check if all hooks (including managed) should be disabled. * This is only true when managed/policy settings has disableAllHooks: true. * When disableAllHooks is set in non-managed settings, managed hooks still run. */ export function shouldDisableAllHooksIncludingManaged(): boolean { return ( settingsModule.getSettingsForSource('policySettings')?.disableAllHooks === true ) } /** * Capture a snapshot of the current hooks configuration * This should be called once during application startup * Respects the allowManagedHooksOnly setting */ export function captureHooksConfigSnapshot(): void { initialHooksConfig = getHooksFromAllowedSources() } /** * Update the hooks configuration snapshot * This should be called when hooks are modified through the settings * Respects the allowManagedHooksOnly setting */ export function updateHooksConfigSnapshot(): void { // Reset the session cache to ensure we read fresh settings from disk. // Without this, the snapshot could use stale cached settings when the user // edits settings.json externally and then runs /hooks - the session cache // may not have been invalidated yet (e.g., if the file watcher's stability // threshold hasn't elapsed). resetSettingsCache() initialHooksConfig = getHooksFromAllowedSources() } /** * Get the current hooks configuration from snapshot * Falls back to settings if no snapshot exists * @returns The hooks configuration */ export function getHooksConfigFromSnapshot(): HooksSettings | null { if (initialHooksConfig === null) { captureHooksConfigSnapshot() } return initialHooksConfig } /** * Reset the hooks configuration snapshot (useful for testing) * Also resets SDK init state to prevent test pollution */ export function resetHooksConfigSnapshot(): void { initialHooksConfig = null resetSdkInitState() }