@@ -34,7 +34,7 @@ import { Registry } from '../../../../../platform/registry/common/platform.js';
3434import { IStorageService , StorageScope , StorageTarget } from '../../../../../platform/storage/common/storage.js' ;
3535import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js' ;
3636import { IExtensionService } from '../../../../services/extensions/common/extensions.js' ;
37- import { IPreToolUseCallerInput } from '../../common/hooks/hooksTypes.js' ;
37+ import { IPreToolUseCallerInput , IPreToolUseHookResult } from '../../common/hooks/hooksTypes.js' ;
3838import { IHooksExecutionService } from '../../common/hooks/hooksExecutionService.js' ;
3939import { ChatContextKeys } from '../../common/actions/chatContextKeys.js' ;
4040import { ChatRequestToolReferenceEntry , toToolSetVariableEntry , toToolVariableEntry } from '../../common/attachments/chatVariableEntries.js' ;
@@ -363,19 +363,21 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
363363
364364 /**
365365 * Execute the preToolUse hook and handle denial.
366- * Returns a tool result if the hook denied execution, or undefined to continue.
366+ * Returns an object containing:
367+ * - denialResult: A tool result if the hook denied execution (caller should return early)
368+ * - hookResult: The full hook result for use in auto-approval logic (allow/ask decisions)
367369 * @param pendingInvocation If there's an existing streaming invocation from beginToolCall, pass it here to cancel it instead of creating a new one.
368370 */
369- private async _executePreToolUseHookAndHandleDenial (
371+ private async _executePreToolUseHook (
370372 dto : IToolInvocation ,
371373 toolData : IToolData | undefined ,
372374 request : IChatRequestModel | undefined ,
373375 pendingInvocation : ChatToolInvocation | undefined ,
374376 token : CancellationToken
375- ) : Promise < IToolResult | undefined > {
377+ ) : Promise < { denialResult ?: IToolResult ; hookResult ?: IPreToolUseHookResult } > {
376378 // Skip hook if no session context or tool doesn't exist
377379 if ( ! dto . context ?. sessionResource || ! toolData ) {
378- return undefined ;
380+ return { } ;
379381 }
380382
381383 const hookInput : IPreToolUseCallerInput = {
@@ -409,12 +411,15 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
409411
410412 const denialMessage = localize ( 'toolExecutionDenied' , "Tool execution denied: {0}" , hookReason ) ;
411413 return {
412- content : [ { kind : 'text' , value : denialMessage } ] ,
413- toolResultError : hookReason ,
414+ denialResult : {
415+ content : [ { kind : 'text' , value : denialMessage } ] ,
416+ toolResultError : hookReason ,
417+ } ,
418+ hookResult,
414419 } ;
415420 }
416421
417- return undefined ;
422+ return { hookResult } ;
418423 }
419424
420425 async invokeTool ( dto : IToolInvocation , countTokens : CountTokensCallback , token : CancellationToken ) : Promise < IToolResult > {
@@ -465,7 +470,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
465470 }
466471
467472 // Execute preToolUse hook - returns early if hook denies execution
468- const hookDenialResult = await this . _executePreToolUseHookAndHandleDenial ( dto , toolData , request , toolInvocation , token ) ;
473+ const { denialResult : hookDenialResult , hookResult : preToolUseHookResult } = await this . _executePreToolUseHook ( dto , toolData , request , toolInvocation , token ) ;
469474 if ( hookDenialResult ) {
470475 // Clean up pending tool call if it exists
471476 if ( pendingToolCallKey ) {
@@ -522,10 +527,11 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
522527 dto . userSelectedTools = request . userSelectedTools && { ...request . userSelectedTools } ;
523528
524529 prepareTimeWatch = StopWatch . create ( true ) ;
525- preparedInvocation = await this . prepareToolInvocation ( tool , dto , token ) ;
530+ preparedInvocation = await this . prepareToolInvocationWithHookResult ( tool , dto , preToolUseHookResult , token ) ;
526531 prepareTimeWatch . stop ( ) ;
527532
528- const autoConfirmed = await this . shouldAutoConfirm ( tool . data . id , tool . data . runsInWorkspace , tool . data . source , dto . parameters , dto . context ?. sessionResource ) ;
533+ const { autoConfirmed, preparedInvocation : updatedPreparedInvocation } = await this . resolveAutoConfirmFromHook ( preToolUseHookResult , tool , dto , preparedInvocation , dto . context ?. sessionResource ) ;
534+ preparedInvocation = updatedPreparedInvocation ;
529535
530536
531537 // Important: a tool invocation that will be autoconfirmed should never
@@ -570,9 +576,12 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
570576 }
571577 } else {
572578 prepareTimeWatch = StopWatch . create ( true ) ;
573- preparedInvocation = await this . prepareToolInvocation ( tool , dto , token ) ;
579+ preparedInvocation = await this . prepareToolInvocationWithHookResult ( tool , dto , preToolUseHookResult , token ) ;
574580 prepareTimeWatch . stop ( ) ;
575- if ( preparedInvocation ?. confirmationMessages ?. title && ! ( await this . shouldAutoConfirm ( tool . data . id , tool . data . runsInWorkspace , tool . data . source , dto . parameters , undefined ) ) ) {
581+
582+ const { autoConfirmed : fallbackAutoConfirmed , preparedInvocation : updatedPreparedInvocation } = await this . resolveAutoConfirmFromHook ( preToolUseHookResult , tool , dto , preparedInvocation , undefined ) ;
583+ preparedInvocation = updatedPreparedInvocation ;
584+ if ( preparedInvocation ?. confirmationMessages ?. title && ! fallbackAutoConfirmed ) {
576585 const result = await this . _dialogService . confirm ( { message : renderAsPlaintext ( preparedInvocation . confirmationMessages . title ) , detail : renderAsPlaintext ( preparedInvocation . confirmationMessages . message ! ) } ) ;
577586 if ( ! result . confirmed ) {
578587 throw new CancellationError ( ) ;
@@ -656,15 +665,77 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
656665 }
657666 }
658667
659- private async prepareToolInvocation ( tool : IToolEntry , dto : IToolInvocation , token : CancellationToken ) : Promise < IPreparedToolInvocation | undefined > {
668+ private async prepareToolInvocationWithHookResult ( tool : IToolEntry , dto : IToolInvocation , hookResult : IPreToolUseHookResult | undefined , token : CancellationToken ) : Promise < IPreparedToolInvocation | undefined > {
669+ let forceConfirmationReason : string | undefined ;
670+ if ( hookResult ?. permissionDecision === 'ask' ) {
671+ const hookMessage = localize ( 'preToolUseHookRequiredConfirmation' , "{0} required confirmation" , HookType . PreToolUse ) ;
672+ forceConfirmationReason = hookResult . permissionDecisionReason
673+ ? `${ hookMessage } : ${ hookResult . permissionDecisionReason } `
674+ : hookMessage ;
675+ }
676+ return this . prepareToolInvocation ( tool , dto , forceConfirmationReason , token ) ;
677+ }
678+
679+ /**
680+ * Determines the auto-confirm decision based on a preToolUse hook result.
681+ * If the hook returned 'allow', auto-approves. If 'ask', forces confirmation
682+ * and ensures confirmation messages exist on `preparedInvocation`. Otherwise
683+ * falls back to normal auto-confirm logic.
684+ *
685+ * Returns the possibly-updated preparedInvocation along with the auto-confirm decision,
686+ * since when the hook returns 'ask' and preparedInvocation was undefined, we create one.
687+ */
688+ private async resolveAutoConfirmFromHook (
689+ hookResult : IPreToolUseHookResult | undefined ,
690+ tool : IToolEntry ,
691+ dto : IToolInvocation ,
692+ preparedInvocation : IPreparedToolInvocation | undefined ,
693+ sessionResource : URI | undefined ,
694+ ) : Promise < { autoConfirmed : ConfirmedReason | undefined ; preparedInvocation : IPreparedToolInvocation | undefined } > {
695+ if ( hookResult ?. permissionDecision === 'allow' ) {
696+ this . _logService . debug ( `[LanguageModelToolsService#invokeTool] Tool ${ dto . toolId } auto-approved by preToolUse hook` ) ;
697+ return { autoConfirmed : { type : ToolConfirmKind . ConfirmationNotNeeded , reason : localize ( 'hookAllowed' , "Allowed by hook" ) } , preparedInvocation } ;
698+ }
699+
700+ if ( hookResult ?. permissionDecision === 'ask' ) {
701+ this . _logService . debug ( `[LanguageModelToolsService#invokeTool] Tool ${ dto . toolId } requires confirmation (preToolUse hook returned 'ask')` ) ;
702+ // Ensure confirmation messages exist when hook requires confirmation
703+ if ( ! preparedInvocation ?. confirmationMessages ?. title ) {
704+ if ( ! preparedInvocation ) {
705+ preparedInvocation = { } ;
706+ }
707+ const fullReferenceName = getToolFullReferenceName ( tool . data ) ;
708+ const hookReason = hookResult . permissionDecisionReason ;
709+ const baseMessage = localize ( 'hookRequiresConfirmation.message' , "{0} hook confirmation required" , HookType . PreToolUse ) ;
710+ preparedInvocation . confirmationMessages = {
711+ ...preparedInvocation . confirmationMessages ,
712+ title : localize ( 'hookRequiresConfirmation.title' , "Use the '{0}' tool?" , fullReferenceName ) ,
713+ message : new MarkdownString ( hookReason ? `${ baseMessage } \n\n${ hookReason } ` : baseMessage ) ,
714+ allowAutoConfirm : false ,
715+ } ;
716+ preparedInvocation . toolSpecificData = {
717+ kind : 'input' ,
718+ rawInput : dto . parameters ,
719+ } ;
720+ }
721+ return { autoConfirmed : undefined , preparedInvocation } ;
722+ }
723+
724+ // No hook decision - use normal auto-confirm logic
725+ const autoConfirmed = await this . shouldAutoConfirm ( tool . data . id , tool . data . runsInWorkspace , tool . data . source , dto . parameters , sessionResource ) ;
726+ return { autoConfirmed, preparedInvocation } ;
727+ }
728+
729+ private async prepareToolInvocation ( tool : IToolEntry , dto : IToolInvocation , forceConfirmationReason : string | undefined , token : CancellationToken ) : Promise < IPreparedToolInvocation | undefined > {
660730 let prepared : IPreparedToolInvocation | undefined ;
661731 if ( tool . impl ! . prepareToolInvocation ) {
662732 const preparePromise = tool . impl ! . prepareToolInvocation ( {
663733 parameters : dto . parameters ,
664734 chatRequestId : dto . chatRequestId ,
665735 chatSessionId : dto . context ?. sessionId ,
666736 chatSessionResource : dto . context ?. sessionResource ,
667- chatInteractionId : dto . chatInteractionId
737+ chatInteractionId : dto . chatInteractionId ,
738+ forceConfirmationReason : forceConfirmationReason
668739 } , token ) ;
669740
670741 const raceResult = await Promise . race ( [
0 commit comments