diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md index 58483baed1..5e50764d2b 100644 --- a/docs/get-started/configuration.md +++ b/docs/get-started/configuration.md @@ -685,11 +685,9 @@ their corresponding top-level category object in your `settings.json` file. - **Requires restart:** Yes - **`tools.enableHooks`** (boolean): - - **Description:** Enable the hooks system for intercepting and customizing - Gemini CLI behavior. When enabled, hooks configured in settings will execute - at appropriate lifecycle events (BeforeTool, AfterTool, BeforeModel, etc.). - Requires MessageBus integration. - - **Default:** `false` + - **Description:** Enables the hooks system experiment. When disabled, the + hooks system is completely deactivated regardless of other settings. + - **Default:** `true` - **Requires restart:** Yes #### `mcp` @@ -867,6 +865,11 @@ their corresponding top-level category object in your `settings.json` file. #### `hooks` +- **`hooks.enabled`** (boolean): + - **Description:** Canonical toggle for the hooks system. When disabled, no + hooks will be executed. + - **Default:** `false` + - **`hooks.disabled`** (array): - **Description:** List of hook names (commands) that should be disabled. Hooks in this list will not execute even if configured. diff --git a/integration-tests/hooks-agent-flow.test.ts b/integration-tests/hooks-agent-flow.test.ts index 45dbb4b0e3..544d4e6072 100644 --- a/integration-tests/hooks-agent-flow.test.ts +++ b/integration-tests/hooks-agent-flow.test.ts @@ -53,10 +53,8 @@ describe('Hooks Agent Flow', () => { await rig.setup('should inject additional context via BeforeAgent hook', { settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, BeforeAgent: [ { hooks: [ @@ -118,10 +116,8 @@ describe('Hooks Agent Flow', () => { await rig.setup('should receive prompt and response in AfterAgent hook', { settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, AfterAgent: [ { hooks: [ @@ -167,10 +163,8 @@ describe('Hooks Agent Flow', () => { 'hooks-agent-flow-multistep.responses', ), settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, BeforeAgent: [ { hooks: [ diff --git a/integration-tests/hooks-system.test.ts b/integration-tests/hooks-system.test.ts index 34827a5f7c..c353f98511 100644 --- a/integration-tests/hooks-system.test.ts +++ b/integration-tests/hooks-system.test.ts @@ -32,10 +32,8 @@ describe('Hooks System Integration', () => { 'hooks-system.block-tool.responses', ), settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, BeforeTool: [ { matcher: 'write_file', @@ -86,10 +84,8 @@ describe('Hooks System Integration', () => { 'hooks-system.allow-tool.responses', ), settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, BeforeTool: [ { matcher: 'write_file', @@ -136,10 +132,8 @@ describe('Hooks System Integration', () => { 'hooks-system.after-tool-context.responses', ), settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, AfterTool: [ { matcher: 'read_file', @@ -211,10 +205,8 @@ console.log(JSON.stringify({ await rig.setup('should modify LLM requests with BeforeModel hooks', { settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, BeforeModel: [ { hooks: [ @@ -294,10 +286,8 @@ console.log(JSON.stringify({ await rig.setup('should modify LLM responses with AfterModel hooks', { settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, AfterModel: [ { hooks: [ @@ -347,10 +337,8 @@ console.log(JSON.stringify({ { settings: { debugMode: true, - tools: { - enableHooks: true, - }, hooks: { + enabled: true, BeforeToolSelection: [ { hooks: [ @@ -415,10 +403,8 @@ console.log(JSON.stringify({ await rig.setup('should augment prompts with BeforeAgent hooks', { settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, BeforeAgent: [ { hooks: [ @@ -460,11 +446,11 @@ console.log(JSON.stringify({ settings: { // Configure tools to enable hooks and require confirmation to trigger notifications tools: { - enableHooks: true, approval: 'ASK', // Disable YOLO mode to show permission prompts confirmationRequired: ['run_shell_command'], }, hooks: { + enabled: true, Notification: [ { matcher: 'ToolPermission', @@ -554,10 +540,8 @@ console.log(JSON.stringify({ 'hooks-system.sequential-execution.responses', ), settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, BeforeAgent: [ { sequential: true, @@ -636,10 +620,8 @@ try { await rig.setup('should provide correct input format to hooks', { settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, BeforeTool: [ { hooks: [ @@ -689,10 +671,8 @@ try { 'hooks-system.multiple-events.responses', ), settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, BeforeAgent: [ { hooks: [ @@ -804,10 +784,8 @@ try { await rig.setup('should handle hook failures gracefully', { settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, BeforeTool: [ { hooks: [ @@ -858,10 +836,8 @@ try { 'hooks-system.telemetry.responses', ), settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, BeforeTool: [ { hooks: [ @@ -901,10 +877,8 @@ try { 'hooks-system.session-startup.responses', ), settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, SessionStart: [ { matcher: 'startup', @@ -974,10 +948,8 @@ console.log(JSON.stringify({ await rig.setup('should fire SessionStart hook and inject context', { settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, SessionStart: [ { matcher: 'startup', @@ -1059,10 +1031,8 @@ console.log(JSON.stringify({ 'should fire SessionStart hook and display systemMessage in interactive mode', { settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, SessionStart: [ { matcher: 'startup', @@ -1129,10 +1099,8 @@ console.log(JSON.stringify({ 'hooks-system.session-clear.responses', ), settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, SessionEnd: [ { matcher: '*', @@ -1303,10 +1271,8 @@ console.log(JSON.stringify({ 'hooks-system.compress-auto.responses', ), settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, PreCompress: [ { matcher: 'auto', @@ -1370,10 +1336,8 @@ console.log(JSON.stringify({ 'hooks-system.session-startup.responses', ), settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, SessionEnd: [ { matcher: 'exit', @@ -1470,10 +1434,8 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho await rig.setup('should not execute hooks disabled in settings file', { settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, BeforeTool: [ { hooks: [ @@ -1552,10 +1514,8 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho 'should respect disabled hooks across multiple operations', { settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, BeforeTool: [ { hooks: [ @@ -1664,10 +1624,8 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho 'hooks-system.input-modification.responses', ), settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, BeforeTool: [ { matcher: 'write_file', @@ -1751,10 +1709,8 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho 'hooks-system.before-tool-stop.responses', ), settings: { - tools: { - enableHooks: true, - }, hooks: { + enabled: true, 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 29811d39b1..03885af651 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 tools.enableHooks to true in your settings to enable the hook system.', + 'Note: Set hooks.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 c2fe65d574..36a1344f74 100644 --- a/packages/cli/src/commands/hooks/migrate.ts +++ b/packages/cli/src/commands/hooks/migrate.ts @@ -243,7 +243,7 @@ export async function handleMigrateFromClaude() { '\nMigration complete! Please review the migrated hooks in .gemini/settings.json', ); debugLogger.log( - 'Note: Set tools.enableHooks to true in your settings to enable the hook system.', + 'Note: Set hooks.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 440f6e7a90..8f233f77fa 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -53,6 +53,7 @@ import { requestConsentNonInteractive } from './extensions/consent.js'; import { promptForSetting } from './extensions/extensionSettings.js'; import type { EventEmitter } from 'node:stream'; import { runExitCleanup } from '../utils/cleanup.js'; +import { getEnableHooks } from './settingsSchema.js'; export interface CliArgs { query: string | undefined; @@ -291,7 +292,7 @@ export async function parseArguments(settings: Settings): Promise { } // Register hooks command if hooks are enabled - if (settings?.tools?.enableHooks) { + if (getEnableHooks(settings)) { yargsInstance.command(hooksCommand); } @@ -722,7 +723,7 @@ export async function loadCliConfig( ptyInfo: ptyInfo?.name, modelConfigServiceConfig: settings.modelConfigs, // TODO: loading of hooks based on workspace trust - enableHooks: settings.tools?.enableHooks, + enableHooks: getEnableHooks(settings), hooks: settings.hooks || {}, projectHooks: projectHooks || {}, onModelChange: (model: string) => saveModelChange(loadedSettings, model), diff --git a/packages/cli/src/config/extension-manager.ts b/packages/cli/src/config/extension-manager.ts index 8c1c8e0a77..4fc9aa6258 100644 --- a/packages/cli/src/config/extension-manager.ts +++ b/packages/cli/src/config/extension-manager.ts @@ -64,6 +64,7 @@ import { type ExtensionSetting, } from './extensions/extensionSettings.js'; import type { EventEmitter } from 'node:stream'; +import { getEnableHooks } from './settingsSchema.js'; interface ExtensionManagerParams { enabledExtensionOverrides?: string[]; @@ -551,7 +552,7 @@ 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) { + if (getEnableHooks(this.settings)) { 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 530d171cd7..9a60b96e40 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -750,8 +750,8 @@ describe('extension tests', () => { ); const settings = loadSettings(tempWorkspaceDir).merged; - if (!settings.tools) settings.tools = {}; - settings.tools.enableHooks = true; + if (!settings.hooks) settings.hooks = {}; + settings.hooks.enabled = true; extensionManager = new ExtensionManager({ workspaceDir: tempWorkspaceDir, @@ -771,7 +771,7 @@ describe('extension tests', () => { ); }); - it('should not load hooks if enableHooks is false', async () => { + it('should not load hooks if hooks.enabled is false', async () => { const extDir = createExtension({ extensionsDir: userExtensionsDir, name: 'hook-extension-disabled', @@ -786,8 +786,8 @@ describe('extension tests', () => { ); const settings = loadSettings(tempWorkspaceDir).merged; - if (!settings.tools) settings.tools = {}; - settings.tools.enableHooks = false; + if (!settings.hooks) settings.hooks = {}; + settings.hooks.enabled = false; extensionManager = new ExtensionManager({ workspaceDir: tempWorkspaceDir, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 08fb30a3cf..ba5f9895cd 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1075,12 +1075,12 @@ const SETTINGS_SCHEMA = { }, enableHooks: { type: 'boolean', - label: 'Enable Hooks System', + label: 'Enable Hooks System (Experimental)', category: 'Advanced', requiresRestart: true, - default: false, + default: true, description: - 'Enable the hooks system for intercepting and customizing Gemini CLI behavior. When enabled, hooks configured in settings will execute at appropriate lifecycle events (BeforeTool, AfterTool, BeforeModel, etc.). Requires MessageBus integration.', + 'Enables the hooks system experiment. When disabled, the hooks system is completely deactivated regardless of other settings.', showInDialog: false, }, }, @@ -1544,6 +1544,16 @@ const SETTINGS_SCHEMA = { 'Hook configurations for intercepting and customizing agent behavior.', showInDialog: false, properties: { + enabled: { + type: 'boolean', + label: 'Enable Hooks', + category: 'Advanced', + requiresRestart: false, + default: false, + description: + 'Canonical toggle for the hooks system. When disabled, no hooks will be executed.', + showInDialog: false, + }, disabled: { type: 'array', label: 'Disabled Hooks', @@ -2057,3 +2067,9 @@ type InferSettings = { }; export type Settings = InferSettings; + +export function getEnableHooks(settings: Settings): boolean { + return ( + (settings.tools?.enableHooks ?? true) && (settings.hooks?.enabled ?? false) + ); +} diff --git a/packages/cli/src/ui/commands/hooksCommand.test.ts b/packages/cli/src/ui/commands/hooksCommand.test.ts index 990228809c..54a3edc991 100644 --- a/packages/cli/src/ui/commands/hooksCommand.test.ts +++ b/packages/cli/src/ui/commands/hooksCommand.test.ts @@ -147,7 +147,7 @@ describe('hooksCommand', () => { type: 'message', messageType: 'info', content: - 'Hook system is not enabled. Enable it in settings with tools.enableHooks', + 'Hook system is not enabled. Enable it in settings with hooks.enabled.', }); }); diff --git a/packages/cli/src/ui/commands/hooksCommand.ts b/packages/cli/src/ui/commands/hooksCommand.ts index 6bbfbb83e7..8028173a84 100644 --- a/packages/cli/src/ui/commands/hooksCommand.ts +++ b/packages/cli/src/ui/commands/hooksCommand.ts @@ -35,7 +35,7 @@ async function panelAction( type: 'message', messageType: 'info', content: - 'Hook system is not enabled. Enable it in settings with tools.enableHooks', + 'Hook system is not enabled. Enable it in settings with hooks.enabled.', }; } diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 1891cfbc60..616743acda 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -753,7 +753,7 @@ export class Config { } // Initialize hook system if enabled - if (this.enableHooks) { + if (this.getEnableHooks()) { this.hookSystem = new HookSystem(this); await this.hookSystem.initialize(); } diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 7c6f1cf3c7..cbf96f738d 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -1129,10 +1129,10 @@ "type": "number" }, "enableHooks": { - "title": "Enable Hooks System", - "description": "Enable the hooks system for intercepting and customizing Gemini CLI behavior. When enabled, hooks configured in settings will execute at appropriate lifecycle events (BeforeTool, AfterTool, BeforeModel, etc.). Requires MessageBus integration.", - "markdownDescription": "Enable the hooks system for intercepting and customizing Gemini CLI behavior. When enabled, hooks configured in settings will execute at appropriate lifecycle events (BeforeTool, AfterTool, BeforeModel, etc.). Requires MessageBus integration.\n\n- Category: `Advanced`\n- Requires restart: `yes`\n- Default: `false`", - "default": false, + "title": "Enable Hooks System (Experimental)", + "description": "Enables the hooks system experiment. When disabled, the hooks system is completely deactivated regardless of other settings.", + "markdownDescription": "Enables the hooks system experiment. When disabled, the hooks system is completely deactivated regardless of other settings.\n\n- Category: `Advanced`\n- Requires restart: `yes`\n- Default: `true`", + "default": true, "type": "boolean" } }, @@ -1502,6 +1502,13 @@ "default": {}, "type": "object", "properties": { + "enabled": { + "title": "Enable Hooks", + "description": "Canonical toggle for the hooks system. When disabled, no hooks will be executed.", + "markdownDescription": "Canonical toggle for the hooks system. When disabled, no hooks will be executed.\n\n- Category: `Advanced`\n- Requires restart: `no`\n- Default: `false`", + "default": false, + "type": "boolean" + }, "disabled": { "title": "Disabled Hooks", "description": "List of hook names (commands) that should be disabled. Hooks in this list will not execute even if configured.",