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

View File

@@ -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` |
| ------------------ | --------------------------- | ------------------------------------------------ | ------- |
| Hook Notifications | `hooksConfig.notifications` | Show visual indicators when hooks are executing. | `true` |
<!-- SETTINGS-AUTOGEN:END -->

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -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 () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -52,7 +52,10 @@ export const StatusDisplay: React.FC<StatusDisplayProps> = ({
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} />;
}

View File

@@ -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', () => {

View File

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

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

View File

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