/ src / utils / hooks / hooksConfigSnapshot.ts
hooksConfigSnapshot.ts
  1  import { resetSdkInitState } from '../../bootstrap/state.js'
  2  import { isRestrictedToPluginOnly } from '../settings/pluginOnlyPolicy.js'
  3  // Import as module object so spyOn works in tests (direct imports bypass spies)
  4  import * as settingsModule from '../settings/settings.js'
  5  import { resetSettingsCache } from '../settings/settingsCache.js'
  6  import type { HooksSettings } from '../settings/types.js'
  7  
  8  let initialHooksConfig: HooksSettings | null = null
  9  
 10  /**
 11   * Get hooks from allowed sources.
 12   * If allowManagedHooksOnly is set in policySettings, only managed hooks are returned.
 13   * If disableAllHooks is set in policySettings, no hooks are returned.
 14   * If disableAllHooks is set in non-managed settings, only managed hooks are returned
 15   * (non-managed settings cannot disable managed hooks).
 16   * Otherwise, returns merged hooks from all sources (backwards compatible).
 17   */
 18  function getHooksFromAllowedSources(): HooksSettings {
 19    const policySettings = settingsModule.getSettingsForSource('policySettings')
 20  
 21    // If managed settings disables all hooks, return empty
 22    if (policySettings?.disableAllHooks === true) {
 23      return {}
 24    }
 25  
 26    // If allowManagedHooksOnly is set in managed settings, only use managed hooks
 27    if (policySettings?.allowManagedHooksOnly === true) {
 28      return policySettings.hooks ?? {}
 29    }
 30  
 31    // strictPluginOnlyCustomization: block user/project/local settings hooks.
 32    // Plugin hooks (registered channel, hooks.ts:1391) are NOT affected —
 33    // they're assembled separately and the managedOnly skip there is keyed
 34    // on shouldAllowManagedHooksOnly(), not on this policy. Agent frontmatter
 35    // hooks are gated at REGISTRATION (runAgent.ts:~535) by agent source —
 36    // plugin/built-in/policySettings agents register normally, user-sourced
 37    // agents skip registration under ["hooks"]. A blanket execution-time
 38    // block here would over-kill plugin agents' hooks.
 39    if (isRestrictedToPluginOnly('hooks')) {
 40      return policySettings?.hooks ?? {}
 41    }
 42  
 43    const mergedSettings = settingsModule.getSettings_DEPRECATED()
 44  
 45    // If disableAllHooks is set in non-managed settings, only managed hooks still run
 46    // (non-managed settings cannot override managed hooks)
 47    if (mergedSettings.disableAllHooks === true) {
 48      return policySettings?.hooks ?? {}
 49    }
 50  
 51    // Otherwise, use all hooks (merged from all sources) - backwards compatible
 52    return mergedSettings.hooks ?? {}
 53  }
 54  
 55  /**
 56   * Check if only managed hooks should run.
 57   * This is true when:
 58   * - policySettings has allowManagedHooksOnly: true, OR
 59   * - disableAllHooks is set in non-managed settings (non-managed settings
 60   *   cannot disable managed hooks, so they effectively become managed-only)
 61   */
 62  export function shouldAllowManagedHooksOnly(): boolean {
 63    const policySettings = settingsModule.getSettingsForSource('policySettings')
 64    if (policySettings?.allowManagedHooksOnly === true) {
 65      return true
 66    }
 67    // If disableAllHooks is set but NOT from managed settings,
 68    // treat as managed-only (non-managed hooks disabled, managed hooks still run)
 69    if (
 70      settingsModule.getSettings_DEPRECATED().disableAllHooks === true &&
 71      policySettings?.disableAllHooks !== true
 72    ) {
 73      return true
 74    }
 75    return false
 76  }
 77  
 78  /**
 79   * Check if all hooks (including managed) should be disabled.
 80   * This is only true when managed/policy settings has disableAllHooks: true.
 81   * When disableAllHooks is set in non-managed settings, managed hooks still run.
 82   */
 83  export function shouldDisableAllHooksIncludingManaged(): boolean {
 84    return (
 85      settingsModule.getSettingsForSource('policySettings')?.disableAllHooks ===
 86      true
 87    )
 88  }
 89  
 90  /**
 91   * Capture a snapshot of the current hooks configuration
 92   * This should be called once during application startup
 93   * Respects the allowManagedHooksOnly setting
 94   */
 95  export function captureHooksConfigSnapshot(): void {
 96    initialHooksConfig = getHooksFromAllowedSources()
 97  }
 98  
 99  /**
100   * Update the hooks configuration snapshot
101   * This should be called when hooks are modified through the settings
102   * Respects the allowManagedHooksOnly setting
103   */
104  export function updateHooksConfigSnapshot(): void {
105    // Reset the session cache to ensure we read fresh settings from disk.
106    // Without this, the snapshot could use stale cached settings when the user
107    // edits settings.json externally and then runs /hooks - the session cache
108    // may not have been invalidated yet (e.g., if the file watcher's stability
109    // threshold hasn't elapsed).
110    resetSettingsCache()
111    initialHooksConfig = getHooksFromAllowedSources()
112  }
113  
114  /**
115   * Get the current hooks configuration from snapshot
116   * Falls back to settings if no snapshot exists
117   * @returns The hooks configuration
118   */
119  export function getHooksConfigFromSnapshot(): HooksSettings | null {
120    if (initialHooksConfig === null) {
121      captureHooksConfigSnapshot()
122    }
123    return initialHooksConfig
124  }
125  
126  /**
127   * Reset the hooks configuration snapshot (useful for testing)
128   * Also resets SDK init state to prevent test pollution
129   */
130  export function resetHooksConfigSnapshot(): void {
131    initialHooksConfig = null
132    resetSdkInitState()
133  }