From 15f1ee8a411058bcbb492c771cba0703580f3a67 Mon Sep 17 00:00:00 2001 From: Geronimo Date: Mon, 14 Sep 2026 21:56:32 +0530 Subject: [PATCH] fix(claw,executor): restore Windows shim launch, close stray coroutine - claw askClaude: prefer native .exe, route only .cmd/.bat through cmd.exe with a quoted command line (DEP0190-safe, same pattern as mcp-health-check); never exec .ps1 directly (not executable without powershell); model allowlist retained so the shell line carries no attacker metacharacters - executor sync path: close un-awaited coroutine before returning the generic failure, eliminating RuntimeWarning noise --- scripts/claw.js | 35 ++++++++++++++++++++++++++--------- src/llm/tools/executor.py | 2 ++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/scripts/claw.js b/scripts/claw.js index b85cfc81c..2f5ff618f 100644 --- a/scripts/claw.js +++ b/scripts/claw.js @@ -95,29 +95,46 @@ function askClaude(systemPrompt, history, userMessage, model) { } args.push('-p'); - // SECURITY: never use shell:true — on Windows Node concatenates command+args - // unquoted (DEP0190), so a model value like `x & calc &` breaks out. - // Validate the model token and spawn without a shell; resolve .cmd shim explicitly. + // SECURITY: a model value like `x & calc &` breaks out when Node + // concatenates command+args unquoted under cmd.exe (DEP0190), so the model + // token is validated and only fixed flags reach the command line. if (model && !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/.test(model)) { return `[Error: invalid model name]`; } + // On Windows the `claude` binary is usually a .cmd shim, which Node + // >=18.20/20.12 refuses to spawn directly (CVE-2024-27980 mitigation), and + // .ps1 shims are not directly executable at all. Resolve a natively + // executable target first; only .cmd/.bat go through cmd.exe, using the + // same quoted-command-line pattern as scripts/hooks/mcp-health-check.js so + // space-containing paths survive as single tokens. .ps1 is never executed + // directly — fall through to bare `claude` (pre-change behavior) instead. + const quoteWin = token => (/[\s"&|<>^%!();]/.test(token) ? '"' + token.replace(/"/g, '""') + '"' : token); let bin = 'claude'; + let useShell = false; if (process.platform === 'win32') { - for (const ext of ['.cmd', '.exe', '.ps1']) { + const { spawnSync: spawnWhere } = require('child_process'); + for (const ext of ['.exe', '.cmd', '.bat']) { + let found = null; try { - const found = require('child_process').spawnSync('where', [`claude${ext}`], { encoding: 'utf8' }); - if (found.status === 0 && found.stdout.trim()) { bin = found.stdout.trim().split(/\r?\n/)[0]; break; } + found = spawnWhere('where', [`claude${ext}`], { encoding: 'utf8' }); } catch { /* ignore */ } + if (found && found.status === 0 && found.stdout && found.stdout.trim()) { + bin = found.stdout.trim().split(/\r?\n/)[0]; + useShell = /\.(cmd|bat)$/i.test(bin); + break; + } } } - const result = spawnSync(bin, args, { + const spawnOpts = { input: fullPrompt, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, CLAUDECODE: '' }, timeout: 300000, - shell: false - }); + }; + const result = useShell + ? spawnSync([bin, ...args].map(quoteWin).join(' '), { ...spawnOpts, shell: true }) + : spawnSync(bin, args, { ...spawnOpts, shell: false }); if (result.error) { return `[Error: ${result.error.message}]`; diff --git a/src/llm/tools/executor.py b/src/llm/tools/executor.py index e59311c1e..9d051e2bf 100644 --- a/src/llm/tools/executor.py +++ b/src/llm/tools/executor.py @@ -78,6 +78,8 @@ class ToolExecutor: "Async tool '%s' called via sync execute(); use execute_async()", tool_call.name, ) + if inspect.iscoroutine(result): + result.close() return ToolResult( tool_call_id=tool_call.id, content=GENERIC_TOOL_FAILURE.format(name=tool_call.name),