feat(hooks): Add a hooks.enabled setting. (#15933)

This commit is contained in:
joshualitt
2026-01-06 13:33:37 -08:00
committed by GitHub
parent c31f05356a
commit 56092bd782
13 changed files with 79 additions and 101 deletions

View File

@@ -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.',
);
});
});

View File

@@ -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)}`);

View File

@@ -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<CliArgs> {
}
// 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),

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<T extends SettingsSchema> = {
};
export type Settings = InferSettings<SettingsSchemaType>;
export function getEnableHooks(settings: Settings): boolean {
return (
(settings.tools?.enableHooks ?? true) && (settings.hooks?.enabled ?? false)
);
}

View File

@@ -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.',
});
});

View File

@@ -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.',
};
}