a batch file saved with Unix line endings makes cmd.exe eat the first byte of lines, so npm becomes pm and powershell becomes hell
encoding · case
Symptom
Section titled “Symptom”A batch script fails with errors naming commands that are not in the file:
'pm' is not recognized as an internal or external command'hell' is not recognized as an internal or external commandThe file plainly says call npm install and powershell -File .... The first
character of the token is gone. Nested if and for blocks silently do not run,
loops break, and none of it is consistent — the same file can fail differently on
different runs.
There is no encoding error, no syntax error, and no mention of line endings anywhere in the output.
The failure needs goto or call — a straight-line script usually survives LF.
Build one whose label sits far enough into the file to cross a 512-byte read
boundary:
PS> $pad = ":: " + ("x" * 600)PS> $body = "@echo off", "goto :main", $pad, ":main", "npm --version"PS> [IO.File]::WriteAllText("t.cmd", ($body -join [char]10) + [char]10)PS> Format-Hex t.cmd | Select-String "0D 0A" # nothing: the file is LF-onlyPS> cmd /c t.cmdRewriting the identical text with CRLF makes it behave:
PS> [IO.File]::WriteAllText("t.cmd", ($body -join "\r\n") + "\r\n")PS> cmd /c t.cmd # runsWhat you get from the LF version varies with where the label lands: a label that
is not found, a block that silently does not execute, or a truncated token
reported as a missing command. npm.cmd itself uses goto, which is why the
reported symptoms name npm.
cmd.exe reads a batch file in chunks as it executes rather than parsing it whole,
and it re-seeks whenever control moves — which is exactly what goto and call
do. The label scanner that performs that seek assumes a two-byte terminator, so on
an LF-only file its arithmetic drifts by one byte per line, and a label that
happens to sit near a chunk boundary is read at the wrong offset.
Straight-line execution mostly tolerates LF, which is why “it worked when I tried it” is such a common and misleading data point. The failure lives in the seek path, so it needs a script that jumps — and it appears or disappears when you add a line anywhere above the label, because that moves where the boundary falls.
That positional sensitivity is what produces the truncated-token symptoms:
resuming at the wrong offset can start mid-word, so call npm ... is reported as
'pm' and powershell as 'hell'. Nothing announces a line-ending problem.
The modern trigger is new. Batch files used to be written by Windows tools that emitted CRLF without being asked. Now they are written by editors defaulting to LF and by AI agents whose file-writing tools emit a bare line feed, so a script shape that worked for twenty years arrives broken from a generator that never considered the question.
Workaround
Section titled “Workaround”Write .cmd and .bat files with CRLF, explicitly:
const body = lines.join("\r\n") + "\r\n";fs.writeFileSync("run.cmd", body); // not join("\n")Pin it in .gitattributes so a checkout cannot undo it:
*.cmd text eol=crlf*.bat text eol=crlfAnd scan for it, since the failure never names itself:
Get-ChildItem -Filter *.cmd -Recurse | Where-Object { -not ([IO.File]::ReadAllBytes($_.FullName) -contains 13)} | Select-Object FullName # LF-only batch filesPowerShell scripts do not share this — .ps1 files handle LF fine. It is
specifically the batch interpreter’s chunked read and label seek.
Verification note
Section titled “Verification note”The originating report (openclaw#119484) is a user account with the symptoms and
a Format-Hex confirmation that the file was LF-only, not an executed
reproduction of the mechanism. The chunk-boundary label-scanner explanation comes
from published analyses of cmd.exe’s batch reader rather than from a run in this
loop; this corpus has no Windows host, hence repro: historical. What is solidly
established is the remedy: batch files want CRLF, and the symptoms disappear when
they get it.
bomless-bat-oem-codepage is the other way a batch file can be byte-wrong: there
the encoding is misread, here the line terminator is. Both produce a
“not recognized” error naming something you never wrote, which is why the pair is
worth knowing together.