Files
Maestro/src/main/process-manager/utils/shellEscape.ts
Pedram Amini cbc772997b fix: address PR review issues for Windows compatibility
- Fix CLI storage path handling: use path.join instead of path.posix.join
  for proper Windows path construction on local filesystem operations

- Fix streamJsonBuilder format: restore correct Claude Code stream-json
  format with type: 'user' and nested message structure (was incorrectly
  changed to type: 'user_message' which causes Claude CLI errors)

- Add shellEscape utility: create proper shell argument escaping module
  with documentation and comprehensive tests for cmd.exe and PowerShell

- Refactor ChildProcessSpawner: use new shellEscape utility instead of
  inline escaping logic for better maintainability

- Fix type errors: correct ToolType usage (remove invalid 'claude' value)
  and add explicit Record<string, AgentSshRemoteConfig> typing
2026-01-31 18:06:23 -05:00

122 lines
3.9 KiB
TypeScript

/**
* Shell argument escaping utilities for Windows cmd.exe and PowerShell.
*
* These functions handle escaping command line arguments to prevent injection
* and ensure proper argument passing when spawning processes via shell.
*
* References:
* - cmd.exe escaping: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/cmd
* - PowerShell escaping: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules
*
* For production use, consider using the 'shescape' npm package which provides
* more comprehensive cross-platform shell escaping with extensive testing.
*/
/**
* Characters that require quoting in cmd.exe.
* Based on cmd.exe special characters: https://ss64.com/nt/syntax-esc.html
*/
const CMD_SPECIAL_CHARS = /[ &|<>^%!()"\n\r#?*]/;
/**
* Characters that require quoting in PowerShell.
* Based on PowerShell special characters: https://ss64.com/ps/syntax-esc.html
*/
const POWERSHELL_SPECIAL_CHARS = /[ &|<>^%!()"\n\r#?*`$@{}[\]';,]/;
/**
* Escape a single argument for use in cmd.exe.
*
* Strategy:
* 1. If the argument contains special characters or is long, wrap in double quotes
* 2. Escape existing double quotes by doubling them
* 3. Escape carets (^) as they're the escape character in cmd.exe
*
* @param arg - The argument to escape
* @returns The escaped argument safe for cmd.exe
*/
export function escapeCmdArg(arg: string): string {
// If no special characters and not too long, return as-is
if (!CMD_SPECIAL_CHARS.test(arg) && arg.length <= 100) {
return arg;
}
// Escape double quotes by doubling them, and carets by doubling
const escaped = arg.replace(/"/g, '""').replace(/\^/g, '^^');
// Wrap in double quotes
return `"${escaped}"`;
}
/**
* Escape a single argument for use in PowerShell.
*
* Strategy:
* 1. If the argument contains special characters or is long, wrap in single quotes
* 2. Escape existing single quotes by doubling them (PowerShell's single-quote escape)
*
* Single quotes in PowerShell treat the content as a literal string, which is
* safer than double quotes (which allow variable expansion).
*
* @param arg - The argument to escape
* @returns The escaped argument safe for PowerShell
*/
export function escapePowerShellArg(arg: string): string {
// If no special characters and not too long, return as-is
if (!POWERSHELL_SPECIAL_CHARS.test(arg) && arg.length <= 100) {
return arg;
}
// Escape single quotes by doubling them (PowerShell's escape mechanism)
const escaped = arg.replace(/'/g, "''");
// Wrap in single quotes (prevents variable expansion)
return `'${escaped}'`;
}
/**
* Escape an array of arguments for use in cmd.exe.
*
* @param args - The arguments to escape
* @returns The escaped arguments safe for cmd.exe
*/
export function escapeCmdArgs(args: string[]): string[] {
return args.map(escapeCmdArg);
}
/**
* Escape an array of arguments for use in PowerShell.
*
* @param args - The arguments to escape
* @returns The escaped arguments safe for PowerShell
*/
export function escapePowerShellArgs(args: string[]): string[] {
return args.map(escapePowerShellArg);
}
/**
* Detect if a shell path refers to PowerShell.
*
* @param shellPath - The shell path to check
* @returns True if the shell is PowerShell (either Windows PowerShell or PowerShell Core)
*/
export function isPowerShellShell(shellPath: string | undefined): boolean {
if (!shellPath) return false;
const lower = shellPath.toLowerCase();
return lower.includes('powershell') || lower.includes('pwsh');
}
/**
* Escape arguments based on the target shell.
*
* @param args - The arguments to escape
* @param shell - The shell path or name (optional, defaults to cmd.exe behavior)
* @returns The escaped arguments
*/
export function escapeArgsForShell(args: string[], shell?: string): string[] {
if (isPowerShellShell(shell)) {
return escapePowerShellArgs(args);
}
return escapeCmdArgs(args);
}