#1261·execa

Execa 10 - Windows: arguments are double caret-escaped for every batch file, not just cmd-shims

Author: timotheeguerinCreated Sep 3, 2026Updated Sep 8, 2026

Draft issue for sindresorhus/execa

Title: Windows: arguments are double caret-escaped for every batch file, not just cmd-shims


Since v10, execa escapes arguments twice for every .cmd/.bat file on Windows. That is right for npm cmd-shims, which re-expand %* through a second cmd.exe parse, but wrong for batch files that read their own arguments: they get the leftover carets and fail. cross-spawn, used through v9, only double-escaped for shims under node_modules/.bin, so this is a regression from #1251.

Steps to reproduce

args.cmd (CRLF):

bat
@echo off
if "%~1" == "-f" (
  echo file is %~2
)
javascript
import {execa} from 'execa';

await execa('./args.cmd', ['-f', String.raw`C:\repo\pom.xml`], {stdio: 'inherit'});

Expected (execa 9.6.1): file is C:\repo\pom.xml

Actual (execa 10.0.1): The syntax of the command is incorrect., exitCode: 255

Cause

lib/arguments/command-file.js decides from the extension alone:

javascript
const batchFileRegExp = /\.(?:bat|cmd)$/i; // line 26
const isDoubleEscape = resolvedFile !== undefined && batchFileRegExp.test(resolvedFile); // line 50

where cross-spawn keyed it off the file being a shim (/node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i). The command lines differ by one level of escaping:

9.6.1  cmd.exe    /d /s /c "…\args.cmd ^"-f^" ^"C:\repo\pom.xml^""
10.0.1 cmd.exe /q /d /s /c "…\args.cmd ^^^"-f^^^" ^^^"C:\repo\pom.xml^^^""

cmd.exe consumes one level when invoking the batch file; a shim consumes the second while expanding %*, which is why shims still work. A batch file that tests its arguments instead sees %1 as ^"-f^", so if "%~1" == "-f" ( expands to if "^"-f^"" == "-f" ( — a syntax error.

Environment

execa 10.0.1 (works on 9.6.1), Node.js 22/24/26, windows-latest GitHub Actions runner.

Found through Maven's mvn.cmd, which parses its own -f argument exactly like the snippet above: it broke every Windows CI job in Azure/typespec-azure and forced a pin back to execa@^9.6.1.