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

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

View File

@@ -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: [

View File

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

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

View File

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

View File

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