one stray quote in PATH makes every entry after it disappear, for your program only — the same shell still finds them
env paths · case
Symptom
Section titled “Symptom”Your program cannot find a tool that is unambiguously installed and on PATH:
Error: failed to run git clone ...: program not foundFrom the same shell, in the same session:
PS> where.exe gitC:\Users\me\AppData\Local\hermes\git\cmd\git.exePS> git --versiongit version 2.54.0.windows.1So the tool is there, the shell finds it, and your process says it does not exist. Every diagnostic you reach for agrees with the shell and against your program, which is why this burns an afternoon.
// PATH = C:\Program Files\PowerShell\7";C:\tools\git\cmd;C:\Windows\System32std::env::split_paths(&std::env::var_os("PATH").unwrap()).count();// the stray quote opens a span that never closes, so everything after it// collapses into ONE entry naming a directory that does not existPS> where.exe gitC:\tools\git\cmd\git.exe # the shell has no troubleThe stray " after 7 is the whole bug. It is trivially easy to produce: a
quoted PATH entry with a typo, an installer that appends without checking, or a
hand-edited environment variable.
Windows PATH entries may be QUOTED, because the separator is ; and a directory
name may legally contain one. So a correct PATH parser has to be quote-aware:
inside quotes, a semicolon is data rather than a separator.
That is exactly what makes an unmatched quote catastrophic. The parser opens a
quoted span at the stray " and never finds its closer, so every remaining
semicolon is swallowed as part of one enormous, nonexistent directory name. Rust’s
std::env::split_paths behaves this way, and it is behaving correctly.
Whether you are affected depends entirely on which splitter you use, and the differences are larger than “quote-aware or not”:
std::env::split_pathshonors a quote ANYWHERE in an entry, so a stray one mid-entry opens a span that swallows every later separator. This is the case that bites.- libuv, which is what Node uses to resolve a command, treats an entry as quoted only when it STARTS with a quote. A mid-entry quote does not open a span there, so Node keeps finding the later entries.
- Naive
split(';')never opens a span at all. where.exeand PowerShell’s own resolution are unaffected.
Three parsers, three behaviors, one PATH. That is the part worth carrying away: “is this PATH valid” has no single answer, so a diagnostic run through a different runtime than the failing program can confirm the wrong thing.
So the failure is not “PATH is broken”. It is “PATH is broken for the correct parsers only”, which inverts the usual debugging instinct: the tools you trust to check are the ones that cannot see the problem.
Workaround
Section titled “Workaround”Fix the PATH — but you usually cannot, because it is the user’s machine. So make the failure legible instead:
let raw = std::env::var_os("PATH").unwrap_or_default();let quotes = raw.to_string_lossy().matches('"').count();if quotes % 2 != 0 { eprintln!("PATH contains an unmatched quote; entries after it are unreadable");}An odd number of quote characters in PATH is always a bug in the PATH, and checking for it costs one line. Reporting THAT instead of “program not found” turns a multi-hour hunt into a one-line fix for the user.
If you must be tolerant, fall back to naive semicolon splitting when the
quote-aware parse yields an entry containing ; — that entry is fictional by
construction.
Verification note
Section titled “Verification note”The std::env::split_paths behavior is read from the Rust standard library
source, and the failure was reported against a Rust binary on Windows
(openai/codex#38421) with where.exe succeeding in the same shell. The
contrasting libuv behavior is read from its process source. Neither was executed
in this loop, hence repro: historical.
node-path-host-delimiter is about the SEPARATOR being wrong (: versus ;).
This is one level deeper: the separator is right, the parse is right, and one
character of user data makes the stricter parser produce a fictional answer while
the looser ones carry on.