From ed66fdd57df73bcc1aa7891e66077ec7248058ee Mon Sep 17 00:00:00 2001 From: Kacper Date: Tue, 30 Dec 2025 15:07:08 +0100 Subject: [PATCH] fix(cursor): Pass prompt via stdin to avoid shell escaping issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When passing file content (containing TypeScript code) to cursor-agent via WSL, bash was interpreting shell metacharacters like $(), backticks, etc. as command substitution, causing errors like "/bin/bash: typescript\r': command not found". Changes: - subprocess.ts: Add stdinData option to SubprocessOptions interface - subprocess.ts: Write stdinData to stdin when provided - cursor-provider.ts: Extract prompt text separately and pass via stdin - cursor-provider.ts: Use '-' as prompt arg to indicate reading from stdin This ensures file content with code examples is passed safely without shell interpretation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/server/src/providers/cursor-provider.ts | 33 ++++++++++++++------ libs/platform/src/subprocess.ts | 23 ++++++++++++-- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/apps/server/src/providers/cursor-provider.ts b/apps/server/src/providers/cursor-provider.ts index 37535dce..14effe8e 100644 --- a/apps/server/src/providers/cursor-provider.ts +++ b/apps/server/src/providers/cursor-provider.ts @@ -114,24 +114,30 @@ export class CursorProvider extends CliProvider { }; } - buildCliArgs(options: ExecuteOptions): string[] { - // Extract model (strip 'cursor-' prefix if present) - const model = stripProviderPrefix(options.model || 'auto'); - - // Build prompt content - let promptText: string; + /** + * Extract prompt text from ExecuteOptions + * Used to pass prompt via stdin instead of CLI args to avoid shell escaping issues + */ + private extractPromptText(options: ExecuteOptions): string { if (typeof options.prompt === 'string') { - promptText = options.prompt; + return options.prompt; } else if (Array.isArray(options.prompt)) { - promptText = options.prompt + return options.prompt .filter((p) => p.type === 'text' && p.text) .map((p) => p.text) .join('\n'); } else { throw new Error('Invalid prompt format'); } + } + + buildCliArgs(options: ExecuteOptions): string[] { + // Extract model (strip 'cursor-' prefix if present) + const model = stripProviderPrefix(options.model || 'auto'); // Build CLI arguments for cursor-agent + // NOTE: Prompt is NOT included here - it's passed via stdin to avoid + // shell escaping issues when content contains $(), backticks, etc. const cliArgs: string[] = [ '-p', // Print mode (non-interactive) '--force', // Allow file modifications @@ -145,8 +151,8 @@ export class CursorProvider extends CliProvider { cliArgs.push('--model', model); } - // Add the prompt - cliArgs.push(promptText); + // Use '-' to indicate reading prompt from stdin + cliArgs.push('-'); return cliArgs; } @@ -439,9 +445,16 @@ export class CursorProvider extends CliProvider { ); } + // Extract prompt text to pass via stdin (avoids shell escaping issues) + const promptText = this.extractPromptText(options); + const cliArgs = this.buildCliArgs(options); const subprocessOptions = this.buildSubprocessOptions(options, cliArgs); + // Pass prompt via stdin to avoid shell interpretation of special characters + // like $(), backticks, etc. that may appear in file content + subprocessOptions.stdinData = promptText; + let sessionId: string | undefined; // Dedup state for Cursor-specific text block handling diff --git a/libs/platform/src/subprocess.ts b/libs/platform/src/subprocess.ts index 4c5afcb0..011d8a62 100644 --- a/libs/platform/src/subprocess.ts +++ b/libs/platform/src/subprocess.ts @@ -12,6 +12,12 @@ export interface SubprocessOptions { env?: Record; abortController?: AbortController; timeout?: number; // Milliseconds of no output before timeout + /** + * Data to write to stdin after process spawns. + * Use this for passing prompts/content that may contain shell metacharacters. + * Avoids shell interpretation issues when passing data as CLI arguments. + */ + stdinData?: string; } export interface SubprocessResult { @@ -24,22 +30,33 @@ export interface SubprocessResult { * Spawns a subprocess and streams JSONL output line-by-line */ export async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGenerator { - const { command, args, cwd, env, abortController, timeout = 30000 } = options; + const { command, args, cwd, env, abortController, timeout = 30000, stdinData } = options; const processEnv = { ...process.env, ...env, }; - console.log(`[SubprocessManager] Spawning: ${command} ${args.slice(0, -1).join(' ')}`); + // Log command without stdin data (which may be large/sensitive) + console.log(`[SubprocessManager] Spawning: ${command} ${args.join(' ')}`); console.log(`[SubprocessManager] Working directory: ${cwd}`); + if (stdinData) { + console.log(`[SubprocessManager] Passing ${stdinData.length} bytes via stdin`); + } const childProcess: ChildProcess = spawn(command, args, { cwd, env: processEnv, - stdio: ['ignore', 'pipe', 'pipe'], + // Use 'pipe' for stdin when we need to write data, otherwise 'ignore' + stdio: [stdinData ? 'pipe' : 'ignore', 'pipe', 'pipe'], }); + // Write stdin data if provided + if (stdinData && childProcess.stdin) { + childProcess.stdin.write(stdinData); + childProcess.stdin.end(); + } + let stderrOutput = ''; let lastOutputTime = Date.now(); let timeoutHandle: NodeJS.Timeout | null = null;