feat(core): Ensure all properties in hooks object are event names. (#16870)

This commit is contained in:
joshualitt
2026-01-20 14:47:31 -08:00
committed by GitHub
parent c9061a1cfe
commit 211d2c5fdd
19 changed files with 180 additions and 93 deletions
+3 -3
View File
@@ -122,10 +122,10 @@ they appear in the UI.
| Enable CLI Help Agent | `experimental.cliHelpAgentSettings.enabled` | Enable the CLI Help Agent. | `true` | | Enable CLI Help Agent | `experimental.cliHelpAgentSettings.enabled` | Enable the CLI Help Agent. | `true` |
| Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` | | Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` |
### Hooks ### HooksConfig
| UI Label | Setting | Description | Default | | UI Label | Setting | Description | Default |
| ------------------ | --------------------- | ------------------------------------------------ | ------- | | ------------------ | --------------------------- | ------------------------------------------------ | ------- |
| Hook Notifications | `hooks.notifications` | Show visual indicators when hooks are executing. | `true` | | Hook Notifications | `hooksConfig.notifications` | Show visual indicators when hooks are executing. | `true` |
<!-- SETTINGS-AUTOGEN:END --> <!-- SETTINGS-AUTOGEN:END -->
+6 -4
View File
@@ -904,22 +904,24 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `[]` - **Default:** `[]`
- **Requires restart:** Yes - **Requires restart:** Yes
#### `hooks` #### `hooksConfig`
- **`hooks.enabled`** (boolean): - **`hooksConfig.enabled`** (boolean):
- **Description:** Canonical toggle for the hooks system. When disabled, no - **Description:** Canonical toggle for the hooks system. When disabled, no
hooks will be executed. hooks will be executed.
- **Default:** `false` - **Default:** `false`
- **`hooks.disabled`** (array): - **`hooksConfig.disabled`** (array):
- **Description:** List of hook names (commands) that should be disabled. - **Description:** List of hook names (commands) that should be disabled.
Hooks in this list will not execute even if configured. Hooks in this list will not execute even if configured.
- **Default:** `[]` - **Default:** `[]`
- **`hooks.notifications`** (boolean): - **`hooksConfig.notifications`** (boolean):
- **Description:** Show visual indicators when hooks are executing. - **Description:** Show visual indicators when hooks are executing.
- **Default:** `true` - **Default:** `true`
#### `hooks`
- **`hooks.BeforeTool`** (array): - **`hooks.BeforeTool`** (array):
- **Description:** Hooks that execute before tool execution. Can intercept, - **Description:** Hooks that execute before tool execution. Can intercept,
validate, or modify tool calls. validate, or modify tool calls.
+9 -3
View File
@@ -53,8 +53,10 @@ describe('Hooks Agent Flow', () => {
await rig.setup('should inject additional context via BeforeAgent hook', { await rig.setup('should inject additional context via BeforeAgent hook', {
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
BeforeAgent: [ BeforeAgent: [
{ {
hooks: [ hooks: [
@@ -116,8 +118,10 @@ describe('Hooks Agent Flow', () => {
await rig.setup('should receive prompt and response in AfterAgent hook', { await rig.setup('should receive prompt and response in AfterAgent hook', {
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
AfterAgent: [ AfterAgent: [
{ {
hooks: [ hooks: [
@@ -163,8 +167,10 @@ describe('Hooks Agent Flow', () => {
'hooks-agent-flow-multistep.responses', 'hooks-agent-flow-multistep.responses',
), ),
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
BeforeAgent: [ BeforeAgent: [
{ {
hooks: [ hooks: [
+83 -29
View File
@@ -32,8 +32,10 @@ describe('Hooks System Integration', () => {
'hooks-system.block-tool.responses', 'hooks-system.block-tool.responses',
), ),
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
BeforeTool: [ BeforeTool: [
{ {
matcher: 'write_file', matcher: 'write_file',
@@ -84,8 +86,10 @@ describe('Hooks System Integration', () => {
'hooks-system.block-tool.responses', 'hooks-system.block-tool.responses',
), ),
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
BeforeTool: [ BeforeTool: [
{ {
matcher: 'write_file', matcher: 'write_file',
@@ -141,8 +145,10 @@ describe('Hooks System Integration', () => {
'hooks-system.allow-tool.responses', 'hooks-system.allow-tool.responses',
), ),
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
BeforeTool: [ BeforeTool: [
{ {
matcher: 'write_file', matcher: 'write_file',
@@ -189,8 +195,10 @@ describe('Hooks System Integration', () => {
'hooks-system.after-tool-context.responses', 'hooks-system.after-tool-context.responses',
), ),
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
AfterTool: [ AfterTool: [
{ {
matcher: 'read_file', matcher: 'read_file',
@@ -262,8 +270,10 @@ console.log(JSON.stringify({
rig.setup('should modify LLM requests with BeforeModel hooks', { rig.setup('should modify LLM requests with BeforeModel hooks', {
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
BeforeModel: [ BeforeModel: [
{ {
hooks: [ hooks: [
@@ -321,8 +331,10 @@ console.log(JSON.stringify({
'should block model execution when BeforeModel hook returns deny decision', 'should block model execution when BeforeModel hook returns deny decision',
{ {
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
BeforeModel: [ BeforeModel: [
{ {
hooks: [ hooks: [
@@ -364,8 +376,10 @@ console.log(JSON.stringify({
'should block model execution when BeforeModel hook returns block decision', 'should block model execution when BeforeModel hook returns block decision',
{ {
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
BeforeModel: [ BeforeModel: [
{ {
hooks: [ hooks: [
@@ -429,8 +443,10 @@ console.log(JSON.stringify({
rig.setup('should modify LLM responses with AfterModel hooks', { rig.setup('should modify LLM responses with AfterModel hooks', {
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
AfterModel: [ AfterModel: [
{ {
hooks: [ hooks: [
@@ -475,8 +491,10 @@ console.log(JSON.stringify({
rig.setup('should modify tool selection with BeforeToolSelection hooks', { rig.setup('should modify tool selection with BeforeToolSelection hooks', {
settings: { settings: {
debugMode: true, debugMode: true,
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
BeforeToolSelection: [ BeforeToolSelection: [
{ {
hooks: [ hooks: [
@@ -540,8 +558,10 @@ console.log(JSON.stringify({
rig.setup('should augment prompts with BeforeAgent hooks', { rig.setup('should augment prompts with BeforeAgent hooks', {
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
BeforeAgent: [ BeforeAgent: [
{ {
hooks: [ hooks: [
@@ -586,8 +606,10 @@ console.log(JSON.stringify({
approval: 'ASK', // Disable YOLO mode to show permission prompts approval: 'ASK', // Disable YOLO mode to show permission prompts
confirmationRequired: ['run_shell_command'], confirmationRequired: ['run_shell_command'],
}, },
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
Notification: [ Notification: [
{ {
matcher: 'ToolPermission', matcher: 'ToolPermission',
@@ -677,8 +699,10 @@ console.log(JSON.stringify({
'hooks-system.sequential-execution.responses', 'hooks-system.sequential-execution.responses',
), ),
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
BeforeAgent: [ BeforeAgent: [
{ {
sequential: true, sequential: true,
@@ -757,8 +781,10 @@ try {
rig.setup('should provide correct input format to hooks', { rig.setup('should provide correct input format to hooks', {
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
BeforeTool: [ BeforeTool: [
{ {
hooks: [ hooks: [
@@ -800,8 +826,10 @@ try {
'hooks-system.allow-tool.responses', 'hooks-system.allow-tool.responses',
), ),
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
BeforeTool: [ BeforeTool: [
{ {
matcher: 'write_file', matcher: 'write_file',
@@ -852,8 +880,10 @@ try {
'hooks-system.multiple-events.responses', 'hooks-system.multiple-events.responses',
), ),
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
BeforeAgent: [ BeforeAgent: [
{ {
hooks: [ hooks: [
@@ -965,8 +995,10 @@ try {
rig.setup('should handle hook failures gracefully', { rig.setup('should handle hook failures gracefully', {
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
BeforeTool: [ BeforeTool: [
{ {
hooks: [ hooks: [
@@ -1017,8 +1049,10 @@ try {
'hooks-system.telemetry.responses', 'hooks-system.telemetry.responses',
), ),
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
BeforeTool: [ BeforeTool: [
{ {
hooks: [ hooks: [
@@ -1058,8 +1092,10 @@ try {
'hooks-system.session-startup.responses', 'hooks-system.session-startup.responses',
), ),
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
SessionStart: [ SessionStart: [
{ {
matcher: 'startup', matcher: 'startup',
@@ -1129,8 +1165,10 @@ console.log(JSON.stringify({
rig.setup('should fire SessionStart hook and inject context', { rig.setup('should fire SessionStart hook and inject context', {
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
SessionStart: [ SessionStart: [
{ {
matcher: 'startup', matcher: 'startup',
@@ -1212,8 +1250,10 @@ console.log(JSON.stringify({
'should fire SessionStart hook and display systemMessage in interactive mode', 'should fire SessionStart hook and display systemMessage in interactive mode',
{ {
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
SessionStart: [ SessionStart: [
{ {
matcher: 'startup', matcher: 'startup',
@@ -1280,8 +1320,10 @@ console.log(JSON.stringify({
'hooks-system.session-clear.responses', 'hooks-system.session-clear.responses',
), ),
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
SessionEnd: [ SessionEnd: [
{ {
matcher: '*', matcher: '*',
@@ -1452,8 +1494,10 @@ console.log(JSON.stringify({
'hooks-system.compress-auto.responses', 'hooks-system.compress-auto.responses',
), ),
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
PreCompress: [ PreCompress: [
{ {
matcher: 'auto', matcher: 'auto',
@@ -1517,8 +1561,10 @@ console.log(JSON.stringify({
'hooks-system.session-startup.responses', 'hooks-system.session-startup.responses',
), ),
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
SessionEnd: [ SessionEnd: [
{ {
matcher: 'exit', 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', { rig.setup('should not execute hooks disabled in settings file', {
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
disabled: [`node "${disabledPath}"`], // Disable the second hook
},
hooks: {
BeforeTool: [ BeforeTool: [
{ {
hooks: [ 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', { rig.setup('should respect disabled hooks across multiple operations', {
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
disabled: [`node "${disabledPath}"`], // Disable the second hook,
},
hooks: {
BeforeTool: [ BeforeTool: [
{ {
hooks: [ 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', 'hooks-system.input-modification.responses',
), ),
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
BeforeTool: [ BeforeTool: [
{ {
matcher: 'write_file', matcher: 'write_file',
@@ -1879,8 +1931,10 @@ console.log(JSON.stringify({decision: "block", systemMessage: "Disabled hook sho
'hooks-system.before-tool-stop.responses', 'hooks-system.before-tool-stop.responses',
), ),
settings: { settings: {
hooks: { hooksConfig: {
enabled: true, enabled: true,
},
hooks: {
BeforeTool: [ BeforeTool: [
{ {
matcher: 'write_file', matcher: 'write_file',
@@ -512,7 +512,7 @@ describe('migrate command', () => {
'\nMigration complete! Please review the migrated hooks in .gemini/settings.json', '\nMigration complete! Please review the migrated hooks in .gemini/settings.json',
); );
expect(debugLoggerLogSpy).toHaveBeenCalledWith( 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.',
); );
}); });
}); });
+5 -2
View File
@@ -230,7 +230,10 @@ export async function handleMigrateFromClaude() {
const settings = loadSettings(workingDir); const settings = loadSettings(workingDir);
// Merge migrated hooks with existing hooks // Merge migrated hooks with existing hooks
const existingHooks = settings.merged.hooks as Record<string, unknown>; const existingHooks = (settings.merged?.hooks || {}) as Record<
string,
unknown
>;
const mergedHooks = { ...existingHooks, ...migratedHooks }; const mergedHooks = { ...existingHooks, ...migratedHooks };
// Update settings (setValue automatically saves) // Update settings (setValue automatically saves)
@@ -242,7 +245,7 @@ export async function handleMigrateFromClaude() {
'\nMigration complete! Please review the migrated hooks in .gemini/settings.json', '\nMigration complete! Please review the migrated hooks in .gemini/settings.json',
); );
debugLogger.log( 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) { } catch (error) {
debugLogger.error(`Error saving migrated hooks: ${getErrorMessage(error)}`); debugLogger.error(`Error saving migrated hooks: ${getErrorMessage(error)}`);
+2 -1
View File
@@ -763,9 +763,10 @@ export async function loadCliConfig(
// TODO: loading of hooks based on workspace trust // TODO: loading of hooks based on workspace trust
enableHooks: enableHooks:
(settings.tools?.enableHooks ?? true) && (settings.tools?.enableHooks ?? true) &&
(settings.hooks?.enabled ?? false), (settings.hooksConfig?.enabled ?? false),
enableHooksUI: settings.tools?.enableHooks ?? true, enableHooksUI: settings.tools?.enableHooks ?? true,
hooks: settings.hooks || {}, hooks: settings.hooks || {},
disabledHooks: settings.hooksConfig?.disabled || [],
projectHooks: projectHooks || {}, projectHooks: projectHooks || {},
onModelChange: (model: string) => saveModelChange(loadedSettings, model), onModelChange: (model: string) => saveModelChange(loadedSettings, model),
onReload: async () => { onReload: async () => {
+4 -1
View File
@@ -613,7 +613,10 @@ Would you like to attempt to install via "git clone" instead?`,
.filter((contextFilePath) => fs.existsSync(contextFilePath)); .filter((contextFilePath) => fs.existsSync(contextFilePath));
let hooks: { [K in HookEventName]?: HookDefinition[] } | undefined; 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, { hooks = await this.loadExtensionHooks(effectiveExtensionPath, {
extensionPath: effectiveExtensionPath, extensionPath: effectiveExtensionPath,
workspacePath: this.workspaceDir, workspacePath: this.workspaceDir,
+3 -3
View File
@@ -815,6 +815,7 @@ describe('extension tests', () => {
fs.mkdirSync(hooksDir); fs.mkdirSync(hooksDir);
const hooksConfig = { const hooksConfig = {
enabled: false,
hooks: { hooks: {
BeforeTool: [ BeforeTool: [
{ {
@@ -836,7 +837,7 @@ describe('extension tests', () => {
); );
const settings = loadSettings(tempWorkspaceDir).merged; const settings = loadSettings(tempWorkspaceDir).merged;
settings.hooks.enabled = true; settings.hooksConfig.enabled = true;
extensionManager = new ExtensionManager({ extensionManager = new ExtensionManager({
workspaceDir: tempWorkspaceDir, workspaceDir: tempWorkspaceDir,
@@ -867,11 +868,10 @@ describe('extension tests', () => {
fs.mkdirSync(hooksDir); fs.mkdirSync(hooksDir);
fs.writeFileSync( fs.writeFileSync(
path.join(hooksDir, 'hooks.json'), path.join(hooksDir, 'hooks.json'),
JSON.stringify({ hooks: { BeforeTool: [] } }), JSON.stringify({ hooks: { BeforeTool: [] }, enabled: false }),
); );
const settings = loadSettings(tempWorkspaceDir).merged; const settings = loadSettings(tempWorkspaceDir).merged;
settings.hooks.enabled = false;
extensionManager = new ExtensionManager({ extensionManager = new ExtensionManager({
workspaceDir: tempWorkspaceDir, workspaceDir: tempWorkspaceDir,
@@ -395,8 +395,8 @@ describe('SettingsSchema', () => {
); );
}); });
it('should have hooks.notifications setting in schema', () => { it('should have hooksConfig.notifications setting in schema', () => {
const setting = getSettingsSchema().hooks.properties.notifications; const setting = getSettingsSchema().hooksConfig?.properties.notifications;
expect(setting).toBeDefined(); expect(setting).toBeDefined();
expect(setting.type).toBe('boolean'); expect(setting.type).toBe('boolean');
expect(setting.category).toBe('Advanced'); expect(setting.category).toBe('Advanced');
+14 -2
View File
@@ -1631,9 +1631,9 @@ const SETTINGS_SCHEMA = {
}, },
}, },
hooks: { hooksConfig: {
type: 'object', type: 'object',
label: 'Hooks', label: 'HooksConfig',
category: 'Advanced', category: 'Advanced',
requiresRestart: false, requiresRestart: false,
default: {}, default: {},
@@ -1675,6 +1675,18 @@ const SETTINGS_SCHEMA = {
description: 'Show visual indicators when hooks are executing.', description: 'Show visual indicators when hooks are executing.',
showInDialog: true, showInDialog: true,
}, },
},
},
hooks: {
type: 'object',
label: 'Hook Events',
category: 'Advanced',
requiresRestart: false,
default: {},
description: 'Event-specific hook configurations.',
showInDialog: false,
properties: {
BeforeTool: { BeforeTool: {
type: 'array', type: 'array',
label: 'Before Tool Hooks', label: 'Before Tool Hooks',
@@ -26,7 +26,7 @@ describe('hooksCommand', () => {
}; };
let mockSettings: { let mockSettings: {
merged: { merged: {
hooks?: { hooksConfig?: {
disabled?: string[]; disabled?: string[];
}; };
tools?: { tools?: {
@@ -58,7 +58,7 @@ describe('hooksCommand', () => {
// Create mock settings // Create mock settings
mockSettings = { mockSettings = {
merged: { merged: {
hooks: { hooksConfig: {
disabled: [], disabled: [],
}, },
}, },
@@ -273,7 +273,7 @@ describe('hooksCommand', () => {
it('should enable a hook and update settings', async () => { it('should enable a hook and update settings', async () => {
// Update the context's settings with disabled hooks // Update the context's settings with disabled hooks
mockContext.services.settings.merged.hooks.disabled = [ mockContext.services.settings.merged.hooksConfig.disabled = [
'test-hook', 'test-hook',
'other-hook', 'other-hook',
]; ];
@@ -289,7 +289,7 @@ describe('hooksCommand', () => {
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith( expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
expect.any(String), expect.any(String),
'hooks.disabled', 'hooksConfig.disabled',
['other-hook'], ['other-hook'],
); );
expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith( expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith(
@@ -404,7 +404,7 @@ describe('hooksCommand', () => {
}); });
it('should disable a hook and update settings', async () => { 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( const disableCmd = hooksCommand.subCommands!.find(
(cmd) => cmd.name === 'disable', (cmd) => cmd.name === 'disable',
@@ -417,7 +417,7 @@ describe('hooksCommand', () => {
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith( expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
expect.any(String), expect.any(String),
'hooks.disabled', 'hooksConfig.disabled',
['test-hook'], ['test-hook'],
); );
expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith( 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 () => { 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 // 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( const disableCmd = hooksCommand.subCommands!.find(
(cmd) => cmd.name === 'disable', (cmd) => cmd.name === 'disable',
@@ -458,7 +458,7 @@ describe('hooksCommand', () => {
}); });
it('should handle error when disabling hook fails', async () => { it('should handle error when disabling hook fails', async () => {
mockContext.services.settings.merged.hooks.disabled = []; mockContext.services.settings.merged.hooksConfig.disabled = [];
mockSettings.setValue.mockImplementationOnce(() => { mockSettings.setValue.mockImplementationOnce(() => {
throw new Error('Failed to save settings'); throw new Error('Failed to save settings');
}); });
@@ -637,7 +637,7 @@ describe('hooksCommand', () => {
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith( expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
expect.any(String), expect.any(String),
'hooks.disabled', 'hooksConfig.disabled',
[], [],
); );
expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith( expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith(
@@ -761,7 +761,7 @@ describe('hooksCommand', () => {
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith( expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
expect.any(String), expect.any(String),
'hooks.disabled', 'hooksConfig.disabled',
['hook-1', 'hook-2', 'hook-3'], ['hook-1', 'hook-2', 'hook-3'],
); );
expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith( expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith(
+10 -10
View File
@@ -76,7 +76,7 @@ async function enableAction(
// Get current disabled hooks from settings // Get current disabled hooks from settings
const settings = context.services.settings; const settings = context.services.settings;
const disabledHooks = settings.merged.hooks.disabled; const disabledHooks = settings.merged.hooksConfig.disabled;
// Remove from disabled list if present // Remove from disabled list if present
const newDisabledHooks = disabledHooks.filter( const newDisabledHooks = disabledHooks.filter(
(name: string) => name !== hookName, (name: string) => name !== hookName,
@@ -87,10 +87,10 @@ async function enableAction(
const scope = settings.workspace const scope = settings.workspace
? SettingScope.Workspace ? SettingScope.Workspace
: SettingScope.User; : 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 // 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 // Enable in hook system
hookSystem.setHookEnabled(hookName, true); hookSystem.setHookEnabled(hookName, true);
@@ -145,7 +145,7 @@ async function disableAction(
// Get current disabled hooks from settings // Get current disabled hooks from settings
const settings = context.services.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 // Add to disabled list if not already present
try { try {
if (!disabledHooks.includes(hookName)) { if (!disabledHooks.includes(hookName)) {
@@ -154,11 +154,11 @@ async function disableAction(
const scope = settings.workspace const scope = settings.workspace
? SettingScope.Workspace ? SettingScope.Workspace
: SettingScope.User; : 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 // 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 // Always disable in hook system to ensure in-memory state matches settings
hookSystem.setHookEnabled(hookName, false); hookSystem.setHookEnabled(hookName, false);
@@ -250,10 +250,10 @@ async function enableAllAction(
const scope = settings.workspace const scope = settings.workspace
? SettingScope.Workspace ? SettingScope.Workspace
: SettingScope.User; : 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 // 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) { for (const hook of disabledHooks) {
const hookName = getHookDisplayName(hook); const hookName = getHookDisplayName(hook);
@@ -323,10 +323,10 @@ async function disableAllAction(
const scope = settings.workspace const scope = settings.workspace
? SettingScope.Workspace ? SettingScope.Workspace
: SettingScope.User; : 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 // 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) { for (const hook of enabledHooks) {
const hookName = getHookDisplayName(hook); const hookName = getHookDisplayName(hook);
@@ -52,7 +52,7 @@ const createMockConfig = (overrides = {}) => ({
const createMockSettings = (merged = {}) => ({ const createMockSettings = (merged = {}) => ({
merged: { merged: {
hooks: { notifications: true }, hooksConfig: { notifications: true },
ui: { hideContextSummary: false }, ui: { hideContextSummary: false },
...merged, ...merged,
}, },
@@ -185,7 +185,7 @@ describe('StatusDisplay', () => {
activeHooks: [{ name: 'hook', eventName: 'event' }], activeHooks: [{ name: 'hook', eventName: 'event' }],
}); });
const settings = createMockSettings({ const settings = createMockSettings({
hooks: { notifications: false }, hooksConfig: { notifications: false },
}); });
const { lastFrame } = renderStatusDisplay( const { lastFrame } = renderStatusDisplay(
{ hideContextSummary: false }, { hideContextSummary: false },
@@ -52,7 +52,10 @@ export const StatusDisplay: React.FC<StatusDisplayProps> = ({
return <Text color={theme.status.error}>{uiState.queueErrorMessage}</Text>; return <Text color={theme.status.error}>{uiState.queueErrorMessage}</Text>;
} }
if (uiState.activeHooks.length > 0 && settings.merged.hooks.notifications) { if (
uiState.activeHooks.length > 0 &&
settings.merged.hooksConfig.notifications
) {
return <HookStatusDisplay activeHooks={uiState.activeHooks} />; return <HookStatusDisplay activeHooks={uiState.activeHooks} />;
} }
+1 -1
View File
@@ -1841,7 +1841,7 @@ describe('Hooks configuration', () => {
debugMode: false, debugMode: false,
model: 'test-model', model: 'test-model',
cwd: '.', cwd: '.',
hooks: { disabled: ['initial-hook'] }, disabledHooks: ['initial-hook'],
}; };
it('updateDisabledHooks should update the disabled list', () => { it('updateDisabledHooks should update the disabled list', () => {
+6 -13
View File
@@ -379,10 +379,9 @@ export interface ConfigParameters {
enableHooks?: boolean; enableHooks?: boolean;
enableHooksUI?: boolean; enableHooksUI?: boolean;
experiments?: Experiments; experiments?: Experiments;
hooks?: { [K in HookEventName]?: HookDefinition[] } & { disabled?: string[] }; hooks?: { [K in HookEventName]?: HookDefinition[] };
projectHooks?: { [K in HookEventName]?: HookDefinition[] } & { disabledHooks?: string[];
disabled?: string[]; projectHooks?: { [K in HookEventName]?: HookDefinition[] };
};
previewFeatures?: boolean; previewFeatures?: boolean;
enableAgents?: boolean; enableAgents?: boolean;
enableEventDrivenScheduler?: boolean; enableEventDrivenScheduler?: boolean;
@@ -680,10 +679,7 @@ export class Config {
: (params.useWriteTodos ?? true); : (params.useWriteTodos ?? true);
this.enableHooksUI = params.enableHooksUI ?? true; this.enableHooksUI = params.enableHooksUI ?? true;
this.enableHooks = params.enableHooks ?? false; this.enableHooks = params.enableHooks ?? false;
this.disabledHooks = this.disabledHooks = params.disabledHooks ?? [];
(params.hooks && 'disabled' in params.hooks
? params.hooks.disabled
: undefined) ?? [];
this.codebaseInvestigatorSettings = { this.codebaseInvestigatorSettings = {
enabled: params.codebaseInvestigatorSettings?.enabled ?? true, enabled: params.codebaseInvestigatorSettings?.enabled ?? true,
@@ -724,8 +720,7 @@ export class Config {
this.disableYoloMode = params.disableYoloMode ?? false; this.disableYoloMode = params.disableYoloMode ?? false;
if (params.hooks) { if (params.hooks) {
const { disabled: _, ...restOfHooks } = params.hooks; this.hooks = params.hooks;
this.hooks = restOfHooks;
} }
if (params.projectHooks) { if (params.projectHooks) {
this.projectHooks = params.projectHooks; this.projectHooks = params.projectHooks;
@@ -1993,9 +1988,7 @@ export class Config {
/** /**
* Get project-specific hooks configuration * Get project-specific hooks configuration
*/ */
getProjectHooks(): getProjectHooks(): { [K in HookEventName]?: HookDefinition[] } | undefined {
| ({ [K in HookEventName]?: HookDefinition[] } & { disabled?: string[] })
| undefined {
return this.projectHooks; return this.projectHooks;
} }
+1 -1
View File
@@ -279,8 +279,8 @@ describe('HookSystem Integration', () => {
], ],
}, },
], ],
disabled: ['echo "disabled-hook"'], // Disable the second hook
}, },
disabledHooks: ['echo "disabled-hook"'], // Disable the second hook
}); });
( (
+12 -2
View File
@@ -1564,8 +1564,8 @@
}, },
"additionalProperties": false "additionalProperties": false
}, },
"hooks": { "hooksConfig": {
"title": "Hooks", "title": "HooksConfig",
"description": "Hook configurations for intercepting and customizing agent behavior.", "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: `{}`", "markdownDescription": "Hook configurations for intercepting and customizing agent behavior.\n\n- Category: `Advanced`\n- Requires restart: `no`\n- Default: `{}`",
"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`", "markdownDescription": "Show visual indicators when hooks are executing.\n\n- Category: `Advanced`\n- Requires restart: `no`\n- Default: `true`",
"default": true, "default": true,
"type": "boolean" "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": { "BeforeTool": {
"title": "Before Tool Hooks", "title": "Before Tool Hooks",
"description": "Hooks that execute before tool execution. Can intercept, validate, or modify tool calls.", "description": "Hooks that execute before tool execution. Can intercept, validate, or modify tool calls.",