diff --git a/docs/cli/settings.md b/docs/cli/settings.md index 2e46dd1fbb..9c6af44ecc 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -122,10 +122,10 @@ they appear in the UI. | Enable CLI Help Agent | `experimental.cliHelpAgentSettings.enabled` | Enable the CLI Help Agent. | `true` | | Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` | -### Hooks +### HooksConfig -| UI Label | Setting | Description | Default | -| ------------------ | --------------------- | ------------------------------------------------ | ------- | -| Hook Notifications | `hooks.notifications` | Show visual indicators when hooks are executing. | `true` | +| UI Label | Setting | Description | Default | +| ------------------ | --------------------------- | ------------------------------------------------ | ------- | +| Hook Notifications | `hooksConfig.notifications` | Show visual indicators when hooks are executing. | `true` | diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md index e71bb3a586..d528e5398a 100644 --- a/docs/get-started/configuration.md +++ b/docs/get-started/configuration.md @@ -904,22 +904,24 @@ their corresponding top-level category object in your `settings.json` file. - **Default:** `[]` - **Requires restart:** Yes -#### `hooks` +#### `hooksConfig` -- **`hooks.enabled`** (boolean): +- **`hooksConfig.enabled`** (boolean): - **Description:** Canonical toggle for the hooks system. When disabled, no hooks will be executed. - **Default:** `false` -- **`hooks.disabled`** (array): +- **`hooksConfig.disabled`** (array): - **Description:** List of hook names (commands) that should be disabled. Hooks in this list will not execute even if configured. - **Default:** `[]` -- **`hooks.notifications`** (boolean): +- **`hooksConfig.notifications`** (boolean): - **Description:** Show visual indicators when hooks are executing. - **Default:** `true` +#### `hooks` + - **`hooks.BeforeTool`** (array): - **Description:** Hooks that execute before tool execution. Can intercept, validate, or modify tool calls. diff --git a/integration-tests/hooks-agent-flow.test.ts b/integration-tests/hooks-agent-flow.test.ts index 544d4e6072..462ec155b0 100644 --- a/integration-tests/hooks-agent-flow.test.ts +++ b/integration-tests/hooks-agent-flow.test.ts @@ -53,8 +53,10 @@ describe('Hooks Agent Flow', () => { await rig.setup('should inject additional context via BeforeAgent hook', { settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { BeforeAgent: [ { hooks: [ @@ -116,8 +118,10 @@ describe('Hooks Agent Flow', () => { await rig.setup('should receive prompt and response in AfterAgent hook', { settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { AfterAgent: [ { hooks: [ @@ -163,8 +167,10 @@ describe('Hooks Agent Flow', () => { 'hooks-agent-flow-multistep.responses', ), settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { BeforeAgent: [ { hooks: [ diff --git a/integration-tests/hooks-system.test.ts b/integration-tests/hooks-system.test.ts index 0f92a16c43..1583f14e77 100644 --- a/integration-tests/hooks-system.test.ts +++ b/integration-tests/hooks-system.test.ts @@ -32,8 +32,10 @@ describe('Hooks System Integration', () => { 'hooks-system.block-tool.responses', ), settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { BeforeTool: [ { matcher: 'write_file', @@ -84,8 +86,10 @@ describe('Hooks System Integration', () => { 'hooks-system.block-tool.responses', ), settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { BeforeTool: [ { matcher: 'write_file', @@ -141,8 +145,10 @@ describe('Hooks System Integration', () => { 'hooks-system.allow-tool.responses', ), settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { BeforeTool: [ { matcher: 'write_file', @@ -189,8 +195,10 @@ describe('Hooks System Integration', () => { 'hooks-system.after-tool-context.responses', ), settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { AfterTool: [ { matcher: 'read_file', @@ -262,8 +270,10 @@ console.log(JSON.stringify({ rig.setup('should modify LLM requests with BeforeModel hooks', { settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { BeforeModel: [ { hooks: [ @@ -321,8 +331,10 @@ console.log(JSON.stringify({ 'should block model execution when BeforeModel hook returns deny decision', { settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { BeforeModel: [ { hooks: [ @@ -364,8 +376,10 @@ console.log(JSON.stringify({ 'should block model execution when BeforeModel hook returns block decision', { settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { BeforeModel: [ { hooks: [ @@ -429,8 +443,10 @@ console.log(JSON.stringify({ rig.setup('should modify LLM responses with AfterModel hooks', { settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { AfterModel: [ { hooks: [ @@ -475,8 +491,10 @@ console.log(JSON.stringify({ rig.setup('should modify tool selection with BeforeToolSelection hooks', { settings: { debugMode: true, - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { BeforeToolSelection: [ { hooks: [ @@ -540,8 +558,10 @@ console.log(JSON.stringify({ rig.setup('should augment prompts with BeforeAgent hooks', { settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { BeforeAgent: [ { hooks: [ @@ -586,8 +606,10 @@ console.log(JSON.stringify({ approval: 'ASK', // Disable YOLO mode to show permission prompts confirmationRequired: ['run_shell_command'], }, - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { Notification: [ { matcher: 'ToolPermission', @@ -677,8 +699,10 @@ console.log(JSON.stringify({ 'hooks-system.sequential-execution.responses', ), settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { BeforeAgent: [ { sequential: true, @@ -757,8 +781,10 @@ try { rig.setup('should provide correct input format to hooks', { settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { BeforeTool: [ { hooks: [ @@ -800,8 +826,10 @@ try { 'hooks-system.allow-tool.responses', ), settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { BeforeTool: [ { matcher: 'write_file', @@ -852,8 +880,10 @@ try { 'hooks-system.multiple-events.responses', ), settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { BeforeAgent: [ { hooks: [ @@ -965,8 +995,10 @@ try { rig.setup('should handle hook failures gracefully', { settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { BeforeTool: [ { hooks: [ @@ -1017,8 +1049,10 @@ try { 'hooks-system.telemetry.responses', ), settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { BeforeTool: [ { hooks: [ @@ -1058,8 +1092,10 @@ try { 'hooks-system.session-startup.responses', ), settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { SessionStart: [ { matcher: 'startup', @@ -1129,8 +1165,10 @@ console.log(JSON.stringify({ rig.setup('should fire SessionStart hook and inject context', { settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { SessionStart: [ { matcher: 'startup', @@ -1212,8 +1250,10 @@ console.log(JSON.stringify({ 'should fire SessionStart hook and display systemMessage in interactive mode', { settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { SessionStart: [ { matcher: 'startup', @@ -1280,8 +1320,10 @@ console.log(JSON.stringify({ 'hooks-system.session-clear.responses', ), settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { SessionEnd: [ { matcher: '*', @@ -1452,8 +1494,10 @@ console.log(JSON.stringify({ 'hooks-system.compress-auto.responses', ), settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { PreCompress: [ { matcher: 'auto', @@ -1517,8 +1561,10 @@ console.log(JSON.stringify({ 'hooks-system.session-startup.responses', ), settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { SessionEnd: [ { matcher: 'exit', @@ -1615,8 +1661,11 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho rig.setup('should not execute hooks disabled in settings file', { settings: { - hooks: { + hooksConfig: { enabled: true, + disabled: [`node "${disabledPath}"`], // Disable the second hook + }, + hooks: { BeforeTool: [ { hooks: [ @@ -1633,7 +1682,6 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho ], }, ], - disabled: [`node "${disabledPath}"`], // Disable the second hook }, }, }); @@ -1690,8 +1738,11 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho rig.setup('should respect disabled hooks across multiple operations', { settings: { - hooks: { + hooksConfig: { enabled: true, + disabled: [`node "${disabledPath}"`], // Disable the second hook, + }, + hooks: { BeforeTool: [ { hooks: [ @@ -1708,7 +1759,6 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho ], }, ], - disabled: [`node "${disabledPath}"`], // Disable the second hook }, }, }); @@ -1795,8 +1845,10 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho 'hooks-system.input-modification.responses', ), settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { BeforeTool: [ { matcher: 'write_file', @@ -1879,8 +1931,10 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho 'hooks-system.before-tool-stop.responses', ), settings: { - hooks: { + hooksConfig: { enabled: true, + }, + hooks: { BeforeTool: [ { matcher: 'write_file', diff --git a/packages/cli/src/commands/hooks/migrate.test.ts b/packages/cli/src/commands/hooks/migrate.test.ts index 03885af651..3f6f5317b4 100644 --- a/packages/cli/src/commands/hooks/migrate.test.ts +++ b/packages/cli/src/commands/hooks/migrate.test.ts @@ -512,7 +512,7 @@ describe('migrate command', () => { '\nMigration complete! Please review the migrated hooks in .gemini/settings.json', ); expect(debugLoggerLogSpy).toHaveBeenCalledWith( - 'Note: Set hooks.enabled to true in your settings to enable the hook system.', + 'Note: Set hooksConfig.enabled to true in your settings to enable the hook system.', ); }); }); diff --git a/packages/cli/src/commands/hooks/migrate.ts b/packages/cli/src/commands/hooks/migrate.ts index 008997d4fe..f1fb56f970 100644 --- a/packages/cli/src/commands/hooks/migrate.ts +++ b/packages/cli/src/commands/hooks/migrate.ts @@ -230,7 +230,10 @@ export async function handleMigrateFromClaude() { const settings = loadSettings(workingDir); // Merge migrated hooks with existing hooks - const existingHooks = settings.merged.hooks as Record; + const existingHooks = (settings.merged?.hooks || {}) as Record< + string, + unknown + >; const mergedHooks = { ...existingHooks, ...migratedHooks }; // Update settings (setValue automatically saves) @@ -242,7 +245,7 @@ export async function handleMigrateFromClaude() { '\nMigration complete! Please review the migrated hooks in .gemini/settings.json', ); debugLogger.log( - 'Note: Set hooks.enabled to true in your settings to enable the hook system.', + 'Note: Set hooksConfig.enabled to true in your settings to enable the hook system.', ); } catch (error) { debugLogger.error(`Error saving migrated hooks: ${getErrorMessage(error)}`); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 1491c4ff44..31cf91412d 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -763,9 +763,10 @@ export async function loadCliConfig( // TODO: loading of hooks based on workspace trust enableHooks: (settings.tools?.enableHooks ?? true) && - (settings.hooks?.enabled ?? false), + (settings.hooksConfig?.enabled ?? false), enableHooksUI: settings.tools?.enableHooks ?? true, hooks: settings.hooks || {}, + disabledHooks: settings.hooksConfig?.disabled || [], projectHooks: projectHooks || {}, onModelChange: (model: string) => saveModelChange(loadedSettings, model), onReload: async () => { diff --git a/packages/cli/src/config/extension-manager.ts b/packages/cli/src/config/extension-manager.ts index 3af4c59f3a..8dbbfe305b 100644 --- a/packages/cli/src/config/extension-manager.ts +++ b/packages/cli/src/config/extension-manager.ts @@ -613,7 +613,10 @@ Would you like to attempt to install via "git clone" instead?`, .filter((contextFilePath) => fs.existsSync(contextFilePath)); let hooks: { [K in HookEventName]?: HookDefinition[] } | undefined; - if (this.settings.tools.enableHooks && this.settings.hooks.enabled) { + if ( + this.settings.tools.enableHooks && + this.settings.hooksConfig.enabled + ) { hooks = await this.loadExtensionHooks(effectiveExtensionPath, { extensionPath: effectiveExtensionPath, workspacePath: this.workspaceDir, diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index 55f44a6c20..78433b6c79 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -815,6 +815,7 @@ describe('extension tests', () => { fs.mkdirSync(hooksDir); const hooksConfig = { + enabled: false, hooks: { BeforeTool: [ { @@ -836,7 +837,7 @@ describe('extension tests', () => { ); const settings = loadSettings(tempWorkspaceDir).merged; - settings.hooks.enabled = true; + settings.hooksConfig.enabled = true; extensionManager = new ExtensionManager({ workspaceDir: tempWorkspaceDir, @@ -867,11 +868,10 @@ describe('extension tests', () => { fs.mkdirSync(hooksDir); fs.writeFileSync( path.join(hooksDir, 'hooks.json'), - JSON.stringify({ hooks: { BeforeTool: [] } }), + JSON.stringify({ hooks: { BeforeTool: [] }, enabled: false }), ); const settings = loadSettings(tempWorkspaceDir).merged; - settings.hooks.enabled = false; extensionManager = new ExtensionManager({ workspaceDir: tempWorkspaceDir, diff --git a/packages/cli/src/config/settingsSchema.test.ts b/packages/cli/src/config/settingsSchema.test.ts index e3bf60deb2..e19bdf3e94 100644 --- a/packages/cli/src/config/settingsSchema.test.ts +++ b/packages/cli/src/config/settingsSchema.test.ts @@ -395,8 +395,8 @@ describe('SettingsSchema', () => { ); }); - it('should have hooks.notifications setting in schema', () => { - const setting = getSettingsSchema().hooks.properties.notifications; + it('should have hooksConfig.notifications setting in schema', () => { + const setting = getSettingsSchema().hooksConfig?.properties.notifications; expect(setting).toBeDefined(); expect(setting.type).toBe('boolean'); expect(setting.category).toBe('Advanced'); diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 779e86cee5..cea502581d 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1631,9 +1631,9 @@ const SETTINGS_SCHEMA = { }, }, - hooks: { + hooksConfig: { type: 'object', - label: 'Hooks', + label: 'HooksConfig', category: 'Advanced', requiresRestart: false, default: {}, @@ -1675,6 +1675,18 @@ const SETTINGS_SCHEMA = { description: 'Show visual indicators when hooks are executing.', showInDialog: true, }, + }, + }, + + hooks: { + type: 'object', + label: 'Hook Events', + category: 'Advanced', + requiresRestart: false, + default: {}, + description: 'Event-specific hook configurations.', + showInDialog: false, + properties: { BeforeTool: { type: 'array', label: 'Before Tool Hooks', diff --git a/packages/cli/src/ui/commands/hooksCommand.test.ts b/packages/cli/src/ui/commands/hooksCommand.test.ts index 5bc9908e14..76a204780a 100644 --- a/packages/cli/src/ui/commands/hooksCommand.test.ts +++ b/packages/cli/src/ui/commands/hooksCommand.test.ts @@ -26,7 +26,7 @@ describe('hooksCommand', () => { }; let mockSettings: { merged: { - hooks?: { + hooksConfig?: { disabled?: string[]; }; tools?: { @@ -58,7 +58,7 @@ describe('hooksCommand', () => { // Create mock settings mockSettings = { merged: { - hooks: { + hooksConfig: { disabled: [], }, }, @@ -273,7 +273,7 @@ describe('hooksCommand', () => { it('should enable a hook and update settings', async () => { // Update the context's settings with disabled hooks - mockContext.services.settings.merged.hooks.disabled = [ + mockContext.services.settings.merged.hooksConfig.disabled = [ 'test-hook', 'other-hook', ]; @@ -289,7 +289,7 @@ describe('hooksCommand', () => { expect(mockContext.services.settings.setValue).toHaveBeenCalledWith( expect.any(String), - 'hooks.disabled', + 'hooksConfig.disabled', ['other-hook'], ); expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith( @@ -404,7 +404,7 @@ describe('hooksCommand', () => { }); it('should disable a hook and update settings', async () => { - mockContext.services.settings.merged.hooks.disabled = []; + mockContext.services.settings.merged.hooksConfig.disabled = []; const disableCmd = hooksCommand.subCommands!.find( (cmd) => cmd.name === 'disable', @@ -417,7 +417,7 @@ describe('hooksCommand', () => { expect(mockContext.services.settings.setValue).toHaveBeenCalledWith( expect.any(String), - 'hooks.disabled', + 'hooksConfig.disabled', ['test-hook'], ); expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith( @@ -433,7 +433,7 @@ describe('hooksCommand', () => { it('should synchronize with hook system even if hook is already in disabled list', async () => { // Update the context's settings with the hook already disabled - mockContext.services.settings.merged.hooks.disabled = ['test-hook']; + mockContext.services.settings.merged.hooksConfig.disabled = ['test-hook']; const disableCmd = hooksCommand.subCommands!.find( (cmd) => cmd.name === 'disable', @@ -458,7 +458,7 @@ describe('hooksCommand', () => { }); it('should handle error when disabling hook fails', async () => { - mockContext.services.settings.merged.hooks.disabled = []; + mockContext.services.settings.merged.hooksConfig.disabled = []; mockSettings.setValue.mockImplementationOnce(() => { throw new Error('Failed to save settings'); }); @@ -637,7 +637,7 @@ describe('hooksCommand', () => { expect(mockContext.services.settings.setValue).toHaveBeenCalledWith( expect.any(String), - 'hooks.disabled', + 'hooksConfig.disabled', [], ); expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith( @@ -761,7 +761,7 @@ describe('hooksCommand', () => { expect(mockContext.services.settings.setValue).toHaveBeenCalledWith( expect.any(String), - 'hooks.disabled', + 'hooksConfig.disabled', ['hook-1', 'hook-2', 'hook-3'], ); expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith( diff --git a/packages/cli/src/ui/commands/hooksCommand.ts b/packages/cli/src/ui/commands/hooksCommand.ts index 0204e1c6e6..7e4221ebfa 100644 --- a/packages/cli/src/ui/commands/hooksCommand.ts +++ b/packages/cli/src/ui/commands/hooksCommand.ts @@ -76,7 +76,7 @@ async function enableAction( // Get current disabled hooks from settings const settings = context.services.settings; - const disabledHooks = settings.merged.hooks.disabled; + const disabledHooks = settings.merged.hooksConfig.disabled; // Remove from disabled list if present const newDisabledHooks = disabledHooks.filter( (name: string) => name !== hookName, @@ -87,10 +87,10 @@ async function enableAction( const scope = settings.workspace ? SettingScope.Workspace : SettingScope.User; - settings.setValue(scope, 'hooks.disabled', newDisabledHooks); + settings.setValue(scope, 'hooksConfig.disabled', newDisabledHooks); // Update core config so re-initialization (e.g. extension reload) respects the change - config.updateDisabledHooks(settings.merged.hooks.disabled); + config.updateDisabledHooks(settings.merged.hooksConfig.disabled); // Enable in hook system hookSystem.setHookEnabled(hookName, true); @@ -145,7 +145,7 @@ async function disableAction( // Get current disabled hooks from settings const settings = context.services.settings; - const disabledHooks = settings.merged.hooks.disabled; + const disabledHooks = settings.merged.hooksConfig.disabled; // Add to disabled list if not already present try { if (!disabledHooks.includes(hookName)) { @@ -154,11 +154,11 @@ async function disableAction( const scope = settings.workspace ? SettingScope.Workspace : SettingScope.User; - settings.setValue(scope, 'hooks.disabled', newDisabledHooks); + settings.setValue(scope, 'hooksConfig.disabled', newDisabledHooks); } // Update core config so re-initialization (e.g. extension reload) respects the change - config.updateDisabledHooks(settings.merged.hooks.disabled); + config.updateDisabledHooks(settings.merged.hooksConfig.disabled); // Always disable in hook system to ensure in-memory state matches settings hookSystem.setHookEnabled(hookName, false); @@ -250,10 +250,10 @@ async function enableAllAction( const scope = settings.workspace ? SettingScope.Workspace : SettingScope.User; - settings.setValue(scope, 'hooks.disabled', []); + settings.setValue(scope, 'hooksConfig.disabled', []); // Update core config so re-initialization (e.g. extension reload) respects the change - config.updateDisabledHooks(settings.merged.hooks.disabled); + config.updateDisabledHooks(settings.merged.hooksConfig.disabled); for (const hook of disabledHooks) { const hookName = getHookDisplayName(hook); @@ -323,10 +323,10 @@ async function disableAllAction( const scope = settings.workspace ? SettingScope.Workspace : SettingScope.User; - settings.setValue(scope, 'hooks.disabled', allHookNames); + settings.setValue(scope, 'hooksConfig.disabled', allHookNames); // Update core config so re-initialization (e.g. extension reload) respects the change - config.updateDisabledHooks(settings.merged.hooks.disabled); + config.updateDisabledHooks(settings.merged.hooksConfig.disabled); for (const hook of enabledHooks) { const hookName = getHookDisplayName(hook); diff --git a/packages/cli/src/ui/components/StatusDisplay.test.tsx b/packages/cli/src/ui/components/StatusDisplay.test.tsx index 8e3bdff68c..1b8cfbdcb9 100644 --- a/packages/cli/src/ui/components/StatusDisplay.test.tsx +++ b/packages/cli/src/ui/components/StatusDisplay.test.tsx @@ -52,7 +52,7 @@ const createMockConfig = (overrides = {}) => ({ const createMockSettings = (merged = {}) => ({ merged: { - hooks: { notifications: true }, + hooksConfig: { notifications: true }, ui: { hideContextSummary: false }, ...merged, }, @@ -185,7 +185,7 @@ describe('StatusDisplay', () => { activeHooks: [{ name: 'hook', eventName: 'event' }], }); const settings = createMockSettings({ - hooks: { notifications: false }, + hooksConfig: { notifications: false }, }); const { lastFrame } = renderStatusDisplay( { hideContextSummary: false }, diff --git a/packages/cli/src/ui/components/StatusDisplay.tsx b/packages/cli/src/ui/components/StatusDisplay.tsx index c9e9e414e5..2259d2c96f 100644 --- a/packages/cli/src/ui/components/StatusDisplay.tsx +++ b/packages/cli/src/ui/components/StatusDisplay.tsx @@ -52,7 +52,10 @@ export const StatusDisplay: React.FC = ({ return {uiState.queueErrorMessage}; } - if (uiState.activeHooks.length > 0 && settings.merged.hooks.notifications) { + if ( + uiState.activeHooks.length > 0 && + settings.merged.hooksConfig.notifications + ) { return ; } diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index 74c0cdac86..b61428315b 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -1841,7 +1841,7 @@ describe('Hooks configuration', () => { debugMode: false, model: 'test-model', cwd: '.', - hooks: { disabled: ['initial-hook'] }, + disabledHooks: ['initial-hook'], }; it('updateDisabledHooks should update the disabled list', () => { diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index cfb7709161..579dcf8d3d 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -379,10 +379,9 @@ export interface ConfigParameters { enableHooks?: boolean; enableHooksUI?: boolean; experiments?: Experiments; - hooks?: { [K in HookEventName]?: HookDefinition[] } & { disabled?: string[] }; - projectHooks?: { [K in HookEventName]?: HookDefinition[] } & { - disabled?: string[]; - }; + hooks?: { [K in HookEventName]?: HookDefinition[] }; + disabledHooks?: string[]; + projectHooks?: { [K in HookEventName]?: HookDefinition[] }; previewFeatures?: boolean; enableAgents?: boolean; enableEventDrivenScheduler?: boolean; @@ -680,10 +679,7 @@ export class Config { : (params.useWriteTodos ?? true); this.enableHooksUI = params.enableHooksUI ?? true; this.enableHooks = params.enableHooks ?? false; - this.disabledHooks = - (params.hooks && 'disabled' in params.hooks - ? params.hooks.disabled - : undefined) ?? []; + this.disabledHooks = params.disabledHooks ?? []; this.codebaseInvestigatorSettings = { enabled: params.codebaseInvestigatorSettings?.enabled ?? true, @@ -724,8 +720,7 @@ export class Config { this.disableYoloMode = params.disableYoloMode ?? false; if (params.hooks) { - const { disabled: _, ...restOfHooks } = params.hooks; - this.hooks = restOfHooks; + this.hooks = params.hooks; } if (params.projectHooks) { this.projectHooks = params.projectHooks; @@ -1993,9 +1988,7 @@ export class Config { /** * Get project-specific hooks configuration */ - getProjectHooks(): - | ({ [K in HookEventName]?: HookDefinition[] } & { disabled?: string[] }) - | undefined { + getProjectHooks(): { [K in HookEventName]?: HookDefinition[] } | undefined { return this.projectHooks; } diff --git a/packages/core/src/hooks/hookSystem.test.ts b/packages/core/src/hooks/hookSystem.test.ts index d4e1edd998..eb71b0cc73 100644 --- a/packages/core/src/hooks/hookSystem.test.ts +++ b/packages/core/src/hooks/hookSystem.test.ts @@ -279,8 +279,8 @@ describe('HookSystem Integration', () => { ], }, ], - disabled: ['echo "disabled-hook"'], // Disable the second hook }, + disabledHooks: ['echo "disabled-hook"'], // Disable the second hook }); ( diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index adcd3b8fb1..c94fc779c8 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -1564,8 +1564,8 @@ }, "additionalProperties": false }, - "hooks": { - "title": "Hooks", + "hooksConfig": { + "title": "HooksConfig", "description": "Hook configurations for intercepting and customizing agent behavior.", "markdownDescription": "Hook configurations for intercepting and customizing agent behavior.\n\n- Category: `Advanced`\n- Requires restart: `no`\n- Default: `{}`", "default": {}, @@ -1594,7 +1594,17 @@ "markdownDescription": "Show visual indicators when hooks are executing.\n\n- Category: `Advanced`\n- Requires restart: `no`\n- Default: `true`", "default": true, "type": "boolean" - }, + } + }, + "additionalProperties": false + }, + "hooks": { + "title": "Hook Events", + "description": "Event-specific hook configurations.", + "markdownDescription": "Event-specific hook configurations.\n\n- Category: `Advanced`\n- Requires restart: `no`\n- Default: `{}`", + "default": {}, + "type": "object", + "properties": { "BeforeTool": { "title": "Before Tool Hooks", "description": "Hooks that execute before tool execution. Can intercept, validate, or modify tool calls.",