Files
ccs/src/utils/shell-executor.ts
T
kaitranntt ed91f21994 fix(shell): escape cmd.exe special chars (%, ^, newlines)
Additional hardening for Windows shell escaping:
- Escape % as %% to prevent variable expansion
- Escape ^ as ^^ to prevent escape sequence interpretation
- Replace newlines/tabs with space to prevent parsing errors
2026-02-05 10:32:31 -05:00

88 lines
2.6 KiB
TypeScript

/**
* Shell Executor Utilities
*
* Cross-platform shell execution utilities for CCS.
*/
import { spawn, ChildProcess } from 'child_process';
import { ErrorManager } from './error-manager';
import { getWebSearchHookEnv } from './websearch-manager';
/**
* Escape arguments for shell execution (cross-platform)
*
* IMPORTANT: On Windows, spawn({ shell: true }) uses cmd.exe by default,
* NOT PowerShell. cmd.exe does NOT recognize single quotes as string delimiters.
* We must use double quotes for cmd.exe compatibility.
*/
export function escapeShellArg(arg: string): string {
const isWindows = process.platform === 'win32';
if (isWindows) {
// cmd.exe: Use double quotes, escape inner double quotes with backslash
// cmd.exe interprets "" as escaped double quote inside quoted string
// Strip newlines/tabs that can break cmd.exe parsing
return (
'"' +
String(arg)
.replace(/[\r\n\t]/g, ' ') // Replace newlines/tabs with space
.replace(/%/g, '%%') // Escape percent signs
.replace(/\^/g, '^^') // Escape carets
.replace(/"/g, '""') + // Escape quotes
'"'
);
} else {
// Unix/macOS: Double quotes with escaped inner quotes
return '"' + String(arg).replace(/"/g, '\\"') + '"';
}
}
/**
* Execute Claude CLI with unified spawn logic
*/
export function execClaude(
claudeCli: string,
args: string[],
envVars: NodeJS.ProcessEnv | null = null
): void {
const isWindows = process.platform === 'win32';
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli);
// Get WebSearch hook config env vars
const webSearchEnv = getWebSearchHookEnv();
// Prepare environment (merge with process.env if envVars provided)
const env = envVars
? { ...process.env, ...envVars, ...webSearchEnv }
: { ...process.env, ...webSearchEnv };
let child: ChildProcess;
if (needsShell) {
// When shell needed: concatenate into string to avoid DEP0190 warning
const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' ');
child = spawn(cmdString, {
stdio: 'inherit',
windowsHide: true,
shell: true,
env,
});
} else {
// When no shell needed: use array form (faster, no shell overhead)
child = spawn(claudeCli, args, {
stdio: 'inherit',
windowsHide: true,
env,
});
}
child.on('exit', (code, signal) => {
if (signal) process.kill(process.pid, signal as NodeJS.Signals);
else process.exit(code || 0);
});
child.on('error', async () => {
await ErrorManager.showClaudeNotFound();
process.exit(1);
});
}