@@ -21,6 +21,7 @@ import {
2121import { getGitInfo } from "./git/index.ts" ;
2222import { createClaudeSender , expandableContent , type DiscordSender , type ClaudeMessage } from "./claude/index.ts" ;
2323import { buildQuestionMessages , parseAskUserButtonId , parseAskUserConfirmId , type AskUserQuestionInput } from "./claude/index.ts" ;
24+ import { buildPermissionEmbed , parsePermissionButtonId , type PermissionRequestCallback } from "./claude/index.ts" ;
2425import { claudeCommands , enhancedClaudeCommands } from "./claude/index.ts" ;
2526import { additionalClaudeCommands } from "./claude/additional-index.ts" ;
2627import { initModels } from "./claude/enhanced-client.ts" ;
@@ -120,6 +121,11 @@ export async function createClaudeCodeBot(config: BotConfig) {
120121 // and waits for the user's click.
121122 // Uses an object wrapper so TypeScript doesn't narrow the closure to `never`.
122123 const askUserState : { handler : ( ( input : AskUserQuestionInput ) => Promise < Record < string , string > > ) | null } = { handler : null } ;
124+
125+ // Late-bound PermissionRequest handler — set after bot is created.
126+ // When Claude wants to use a tool that isn't pre-approved, this shows
127+ // Allow/Deny buttons in Discord and returns the user's decision.
128+ const permReqState : { handler : PermissionRequestCallback | null } = { handler : null } ;
123129
124130 // Create sendClaudeMessages function that uses the sender when available
125131 const sendClaudeMessages = async ( messages : ClaudeMessage [ ] ) => {
@@ -136,6 +142,15 @@ export async function createClaudeCodeBot(config: BotConfig) {
136142 return await askUserState . handler ( input ) ;
137143 } ;
138144
145+ // Create onPermissionRequest wrapper — delegates to permReqState.handler once bot is ready
146+ const onPermissionRequest : PermissionRequestCallback = async ( toolName , toolInput ) => {
147+ if ( ! permReqState . handler ) {
148+ console . warn ( '[PermissionRequest] Handler not initialized — auto-denying' ) ;
149+ return false ;
150+ }
151+ return await permReqState . handler ( toolName , toolInput ) ;
152+ } ;
153+
139154 // Create all handlers using the registry (centralized handler creation)
140155 const allHandlers : AllHandlers = createAllHandlers (
141156 {
@@ -153,6 +168,7 @@ export async function createClaudeCodeBot(config: BotConfig) {
153168 claudeSessionManager,
154169 sendClaudeMessages,
155170 onAskUser,
171+ onPermissionRequest,
156172 onBotSettingsUpdate : ( settings ) => {
157173 botSettings . mentionEnabled = settings . mentionEnabled ;
158174 botSettings . mentionUserId = settings . mentionUserId ;
@@ -212,6 +228,9 @@ export async function createClaudeCodeBot(config: BotConfig) {
212228
213229 // Initialize AskUserQuestion handler — sends questions to Discord, waits for button clicks
214230 askUserState . handler = createAskUserDiscordHandler ( bot ) ;
231+
232+ // Initialize PermissionRequest handler — shows Allow/Deny buttons for unapproved tools
233+ permReqState . handler = createPermissionRequestHandler ( bot ) ;
215234
216235 // Check for updates (non-blocking)
217236 runVersionCheck ( ) . then ( async ( { updateAvailable, embed } ) => {
@@ -431,6 +450,81 @@ function createAskUserDiscordHandler(bot: any): (input: AskUserQuestionInput) =>
431450 return answers ;
432451 } ;
433452}
453+
454+ /**
455+ * Create the PermissionRequest handler that uses the Discord channel.
456+ *
457+ * When Claude wants to use a tool that isn't pre-approved:
458+ * 1. Builds an embed showing the tool name and input preview
459+ * 2. Adds Allow / Deny buttons
460+ * 3. Sends to the bot's channel
461+ * 4. Waits for a button click (no timeout — user decides)
462+ * 5. Returns true (allow) or false (deny)
463+ */
464+ // deno-lint-ignore no-explicit-any
465+ function createPermissionRequestHandler ( bot : any ) : PermissionRequestCallback {
466+ // Simple incrementing nonce to disambiguate concurrent requests
467+ let nonce = 0 ;
468+
469+ return async ( toolName : string , toolInput : Record < string , unknown > ) : Promise < boolean > => {
470+ const channel = bot . getChannel ( ) ;
471+ if ( ! channel ) {
472+ console . warn ( '[PermissionRequest] No channel — auto-denying' ) ;
473+ return false ;
474+ }
475+
476+ const reqNonce = String ( ++ nonce ) ;
477+ const embedData = buildPermissionEmbed ( toolName , toolInput ) ;
478+
479+ const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType } = await import ( "npm:discord.js@14.14.1" ) ;
480+
481+ const embed = new EmbedBuilder ( )
482+ . setColor ( embedData . color )
483+ . setTitle ( embedData . title )
484+ . setDescription ( embedData . description )
485+ . setFooter ( { text : embedData . footer . text } )
486+ . setTimestamp ( ) ;
487+
488+ for ( const field of embedData . fields ) {
489+ embed . addFields ( { name : field . name , value : field . value , inline : field . inline } ) ;
490+ }
491+
492+ const row = new ActionRowBuilder ( ) . addComponents (
493+ new ButtonBuilder ( )
494+ . setCustomId ( `perm-req:${ reqNonce } :allow` )
495+ . setLabel ( '✅ Allow' )
496+ . setStyle ( ButtonStyle . Success ) ,
497+ new ButtonBuilder ( )
498+ . setCustomId ( `perm-req:${ reqNonce } :deny` )
499+ . setLabel ( '❌ Deny' )
500+ . setStyle ( ButtonStyle . Danger ) ,
501+ ) ;
502+
503+ const msg = await channel . send ( { embeds : [ embed ] , components : [ row ] } ) ;
504+
505+ // Wait for exactly one button click — no timeout
506+ // deno-lint-ignore no-explicit-any
507+ const interaction : any = await msg . awaitMessageComponent ( {
508+ componentType : ComponentType . Button ,
509+ } ) ;
510+
511+ const parsed = parsePermissionButtonId ( interaction . customId ) ;
512+ const allowed = parsed ?. allowed ?? false ;
513+
514+ // Update the embed to reflect the decision
515+ embed . setColor ( allowed ? 0x00ff00 : 0xff4444 )
516+ . setFooter ( { text : allowed ? `✅ Allowed by user` : `❌ Denied by user` } ) ;
517+
518+ await interaction . update ( {
519+ embeds : [ embed ] ,
520+ components : [ ] , // Remove buttons after decision
521+ } ) ;
522+
523+ console . log ( `[PermissionRequest] Tool "${ toolName } " — ${ allowed ? 'ALLOWED' : 'DENIED' } by user` ) ;
524+ return allowed ;
525+ } ;
526+ }
527+
434528/**
435529 * Setup signal handlers for graceful shutdown.
436530 */
0 commit comments