@@ -56,6 +56,15 @@ type UserContent = Array<
5656 Anthropic . TextBlockParam | Anthropic . ImageBlockParam | Anthropic . ToolUseBlockParam | Anthropic . ToolResultBlockParam
5757>
5858
59+ // Add near the top of the file, after imports:
60+ const ALLOWED_AUTO_EXECUTE_COMMANDS = [
61+ 'npm' ,
62+ 'npx' ,
63+ 'tsc' ,
64+ 'git log' ,
65+ 'git diff'
66+ ] as const
67+
5968export class Cline {
6069 readonly taskId : string
6170 api : ApiHandler
@@ -124,6 +133,14 @@ export class Cline {
124133 }
125134 }
126135
136+ protected isAllowedCommand ( command ?: string ) : boolean {
137+ if ( ! command ) return false ;
138+ const trimmedCommand = command . trim ( ) . toLowerCase ( ) ;
139+ return ALLOWED_AUTO_EXECUTE_COMMANDS . some ( prefix =>
140+ trimmedCommand . startsWith ( prefix . toLowerCase ( ) )
141+ ) ;
142+ }
143+
127144 // Storing task to disk for history
128145
129146 private async ensureTaskDirectoryExists ( ) : Promise < string > {
@@ -555,8 +572,8 @@ export class Cline {
555572 : [ { type : "text" , text : lastMessage . content } ]
556573 if ( previousAssistantMessage && previousAssistantMessage . role === "assistant" ) {
557574 const assistantContent = Array . isArray ( previousAssistantMessage . content )
558- ? previousAssistantMessage . content
559- : [ { type : "text" , text : previousAssistantMessage . content } ]
575+ ? previousAssistantMessage . content
576+ : [ { type : "text" , text : previousAssistantMessage . content } ]
560577
561578 const toolUseBlocks = assistantContent . filter (
562579 ( block ) => block . type === "tool_use"
@@ -839,7 +856,7 @@ export class Cline {
839856 // (have to do this for partial and complete since sending content in thinking tags to markdown renderer will automatically be removed)
840857 // Remove end substrings of <thinking or </thinking (below xml parsing is only for opening tags)
841858 // (this is done with the xml parsing below now, but keeping here for reference)
842- // content = content.replace(/<\/?t(?:h(?:i(?:n(?:k(?:i(?:n(?:g)?)?)?)?)?)?)? $/, "")
859+ // content = content.replace(/<\/?t(?:h(?:i(?:n(?:k(?:i(?:n(?:g)?)?)?)?)?$/, "")
843860 // Remove all instances of <thinking> (with optional line break after) and </thinking> (with optional line break before)
844861 // - Needs to be separate since we dont want to remove the line break before the first tag
845862 // - Needs to happen before the xml parsing below
@@ -1503,7 +1520,7 @@ export class Cline {
15031520 const command : string | undefined = block . params . command
15041521 try {
15051522 if ( block . partial ) {
1506- if ( this . alwaysAllowExecute ) {
1523+ if ( this . alwaysAllowExecute && this . isAllowedCommand ( command ) ) {
15071524 await this . say ( "command" , command , undefined , block . partial )
15081525 } else {
15091526 await this . ask ( "command" , removeClosingTag ( "command" , command ) , block . partial ) . catch (
@@ -1520,7 +1537,9 @@ export class Cline {
15201537 break
15211538 }
15221539 this . consecutiveMistakeCount = 0
1523- const didApprove = this . alwaysAllowExecute || ( await askApproval ( "command" , command ) )
1540+
1541+ const didApprove = ( this . alwaysAllowExecute && this . isAllowedCommand ( command ) ) ||
1542+ ( await askApproval ( "command" , command ) )
15241543 if ( ! didApprove ) {
15251544 break
15261545 }
0 commit comments