( input: z.infer<typeof BashTool.inputSchema>, cwd: string, toolPermissionContext: ToolPermissionContext, compoundCommandHasCd?: boolean, astRedirects?: Redirect[], astCommands?: SimpleCommand[], )
| 1011 | * - 'passthrough' if no path commands were found or if all are within allowed directories |
| 1012 | */ |
| 1013 | export function checkPathConstraints( |
| 1014 | input: z.infer<typeof BashTool.inputSchema>, |
| 1015 | cwd: string, |
| 1016 | toolPermissionContext: ToolPermissionContext, |
| 1017 | compoundCommandHasCd?: boolean, |
| 1018 | astRedirects?: Redirect[], |
| 1019 | astCommands?: SimpleCommand[], |
| 1020 | ): PermissionResult { |
| 1021 | // SECURITY: Process substitution >(cmd) can execute commands that write to files |
| 1022 | // without those files appearing as redirect targets. For example: |
| 1023 | // echo secret > >(tee .git/config) |
| 1024 | // The tee command writes to .git/config but it's not detected as a redirect. |
| 1025 | // Require explicit approval for any command containing process substitution. |
| 1026 | // Skip on AST path — process_substitution is in DANGEROUS_TYPES and |
| 1027 | // already returned too-complex before reaching here. |
| 1028 | if (!astCommands && />>\s*>\s*\(|>\s*>\s*\(|<\s*\(/.test(input.command)) { |
| 1029 | return { |
| 1030 | behavior: 'ask', |
| 1031 | message: |
| 1032 | 'Process substitution (>(...) or <(...)) can execute arbitrary commands and requires manual approval', |
| 1033 | decisionReason: { |
| 1034 | type: 'other', |
| 1035 | reason: 'Process substitution requires manual approval', |
| 1036 | }, |
| 1037 | } |
| 1038 | } |
| 1039 | |
| 1040 | // SECURITY: When AST-derived redirects are available, use them directly |
| 1041 | // instead of re-parsing with shell-quote. shell-quote has a known |
| 1042 | // single-quote backslash bug that silently merges redirect operators into |
| 1043 | // garbled tokens on a successful parse (not a parse failure, so the |
| 1044 | // fail-closed guard doesn't help). The AST already resolved targets |
| 1045 | // correctly and checkSemantics validated them. |
| 1046 | const { redirections, hasDangerousRedirection } = astRedirects |
| 1047 | ? astRedirectsToOutputRedirections(astRedirects) |
| 1048 | : extractOutputRedirections(input.command) |
| 1049 | |
| 1050 | // SECURITY: If we found a redirection operator with a target containing shell expansion |
| 1051 | // syntax ($VAR or %VAR%), require manual approval since the target can't be safely validated. |
| 1052 | if (hasDangerousRedirection) { |
| 1053 | return { |
| 1054 | behavior: 'ask', |
| 1055 | message: 'Shell expansion syntax in paths requires manual approval', |
| 1056 | decisionReason: { |
| 1057 | type: 'other', |
| 1058 | reason: 'Shell expansion syntax in paths requires manual approval', |
| 1059 | }, |
| 1060 | } |
| 1061 | } |
| 1062 | const redirectionResult = validateOutputRedirections( |
| 1063 | redirections, |
| 1064 | cwd, |
| 1065 | toolPermissionContext, |
| 1066 | compoundCommandHasCd, |
| 1067 | ) |
| 1068 | if (redirectionResult.behavior !== 'passthrough') { |
| 1069 | return redirectionResult |
| 1070 | } |
no test coverage detected