@@ -44,7 +44,7 @@ type ShellToken = string | { op: string } | { command: string }
4444 *
4545 * ## Security Considerations:
4646 *
47- * - **Subshell Protection**: Prevents command injection via $(command) or `command`
47+ * - **Subshell Protection**: Prevents command injection via $(command), `command`, or process substitution
4848 * - **Chain Analysis**: Each command in a chain (cmd1 && cmd2) is validated separately
4949 * - **Case Insensitive**: All matching is case-insensitive for consistency
5050 * - **Whitespace Handling**: Commands are trimmed and normalized before matching
@@ -58,13 +58,26 @@ type ShellToken = string | { op: string } | { command: string }
5858 * This allows users to have personal defaults while projects can define specific restrictions.
5959 */
6060
61+ /**
62+ * Detect subshell usage and command substitution patterns:
63+ * - $() - command substitution
64+ * - `` - backticks (legacy command substitution)
65+ * - <() - process substitution (input)
66+ * - >() - process substitution (output)
67+ * - $(()) - arithmetic expansion
68+ * - $[] - arithmetic expansion (alternative syntax)
69+ */
70+ export function containsSubshell ( source : string ) : boolean {
71+ return / ( \$ \( ) | ` | < \( | > \( | \$ \[ / . test ( source )
72+ }
73+
6174/**
6275 * Split a command string into individual sub-commands by
6376 * chaining operators (&&, ||, ;, or |) and newlines.
6477 *
6578 * Uses shell-quote to properly handle:
6679 * - Quoted strings (preserves quotes)
67- * - Subshell commands ($(cmd) or `cmd`)
80+ * - Subshell commands ($(cmd), `cmd`, <(cmd), >(cmd) )
6881 * - PowerShell redirections (2>&1)
6982 * - Chain operators (&&, ||, ;, |)
7083 * - Newlines as command separators
@@ -103,7 +116,6 @@ function parseCommandLine(command: string): string[] {
103116 const arithmeticExpressions : string [ ] = [ ]
104117 const variables : string [ ] = [ ]
105118 const parameterExpansions : string [ ] = [ ]
106- const processSubstitutions : string [ ] = [ ]
107119
108120 // First handle PowerShell redirections by temporarily replacing them
109121 let processedCommand = command . replace ( / \d * > & \d * / g, ( match ) => {
@@ -118,6 +130,12 @@ function parseCommandLine(command: string): string[] {
118130 return `__ARITH_${ arithmeticExpressions . length - 1 } __`
119131 } )
120132
133+ // Handle $[...] arithmetic expressions (alternative syntax)
134+ processedCommand = processedCommand . replace ( / \$ \[ [ ^ \] ] * \] / g, ( match ) => {
135+ arithmeticExpressions . push ( match )
136+ return `__ARITH_${ arithmeticExpressions . length - 1 } __`
137+ } )
138+
121139 // Handle parameter expansions: ${...} patterns (including array indexing)
122140 // This covers ${var}, ${var:-default}, ${var:+alt}, ${#var}, ${var%pattern}, etc.
123141 processedCommand = processedCommand . replace ( / \$ \{ [ ^ } ] + \} / g, ( match ) => {
@@ -126,9 +144,9 @@ function parseCommandLine(command: string): string[] {
126144 } )
127145
128146 // Handle process substitutions: <(...) and >(...)
129- processedCommand = processedCommand . replace ( / [ < > ] \( [ ^ ) ] + \) / g, ( match ) => {
130- processSubstitutions . push ( match )
131- return `__PROCSUB_ ${ processSubstitutions . length - 1 } __`
147+ processedCommand = processedCommand . replace ( / [ < > ] \( ( [ ^ ) ] + ) \) / g, ( _ , inner ) => {
148+ subshells . push ( inner . trim ( ) )
149+ return `__SUBSH_ ${ subshells . length - 1 } __`
132150 } )
133151
134152 // Handle simple variable references: $varname pattern
@@ -144,7 +162,7 @@ function parseCommandLine(command: string): string[] {
144162 return `__VAR_${ variables . length - 1 } __`
145163 } )
146164
147- // Then handle subshell commands
165+ // Then handle subshell commands $() and back-ticks
148166 processedCommand = processedCommand
149167 . replace ( / \$ \( ( .* ?) \) / g, ( _ , inner ) => {
150168 subshells . push ( inner . trim ( ) )
@@ -187,10 +205,9 @@ function parseCommandLine(command: string): string[] {
187205 result = result . replace ( / _ _ A R I T H _ ( \d + ) _ _ / g, ( _ , i ) => arithmeticExpressions [ parseInt ( i ) ] )
188206 // Restore parameter expansions
189207 result = result . replace ( / _ _ P A R A M _ ( \d + ) _ _ / g, ( _ , i ) => parameterExpansions [ parseInt ( i ) ] )
190- // Restore process substitutions
191- result = result . replace ( / _ _ P R O C S U B _ ( \d + ) _ _ / g, ( _ , i ) => processSubstitutions [ parseInt ( i ) ] )
192208 // Restore variable references
193209 result = result . replace ( / _ _ V A R _ ( \d + ) _ _ / g, ( _ , i ) => variables [ parseInt ( i ) ] )
210+ result = result . replace ( / _ _ S U B S H _ ( \d + ) _ _ / g, ( _ , i ) => subshells [ parseInt ( i ) ] )
194211 return result
195212 } )
196213 }
@@ -243,10 +260,9 @@ function parseCommandLine(command: string): string[] {
243260 result = result . replace ( / _ _ A R I T H _ ( \d + ) _ _ / g, ( _ , i ) => arithmeticExpressions [ parseInt ( i ) ] )
244261 // Restore parameter expansions
245262 result = result . replace ( / _ _ P A R A M _ ( \d + ) _ _ / g, ( _ , i ) => parameterExpansions [ parseInt ( i ) ] )
246- // Restore process substitutions
247- result = result . replace ( / _ _ P R O C S U B _ ( \d + ) _ _ / g, ( _ , i ) => processSubstitutions [ parseInt ( i ) ] )
248263 // Restore variable references
249264 result = result . replace ( / _ _ V A R _ ( \d + ) _ _ / g, ( _ , i ) => variables [ parseInt ( i ) ] )
265+ result = result . replace ( / _ _ S U B S H _ ( \d + ) _ _ / g, ( _ , i ) => subshells [ parseInt ( i ) ] )
250266 return result
251267 } )
252268}
@@ -430,14 +446,6 @@ export function getCommandDecision(
430446) : CommandDecision {
431447 if ( ! command ?. trim ( ) ) return "auto_approve"
432448
433- // Check if subshells contain denied prefixes
434- if ( ( command . includes ( "$(" ) || command . includes ( "`" ) ) && deniedCommands ?. length ) {
435- const mainCommandLower = command . toLowerCase ( )
436- if ( deniedCommands . some ( ( denied ) => mainCommandLower . includes ( denied . toLowerCase ( ) ) ) ) {
437- return "auto_deny"
438- }
439- }
440-
441449 // Parse into sub-commands (split by &&, ||, ;, |)
442450 const subCommands = parseCommand ( command )
443451
@@ -610,7 +618,7 @@ export class CommandValidator {
610618 hasSubshells : boolean
611619 } {
612620 const subCommands = parseCommand ( command )
613- const hasSubshells = command . includes ( "$(" ) || command . includes ( "`" )
621+ const hasSubshells = containsSubshell ( command )
614622
615623 const allowedMatches = subCommands . map ( ( cmd ) => ( {
616624 command : cmd ,
0 commit comments