hermes - 💡(How to fix) Fix [Bug] Windows: asyncio.subprocess_exec misparses argv metacharacters when target is a .cmd/.bat shim

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

On Windows, when Hermes spawns a CLI tool that is exposed via a .cmd / .bat shim (very common — claude.cmd, npm.cmd, gh.cmd, many others), asyncio.create_subprocess_exec() may pass argv into the shim in a form that causes the shim's cmd.exe interpreter to re-parse shell metacharacters embedded in user/agent-supplied arguments.

Symptoms range from broken behavior (e.g., |, >, &, ^, " in a prompt arg silently truncate or trigger pipes / redirects) to outright command injection if untrusted content reaches the argv.

Root Cause

On Windows, when Hermes spawns a CLI tool that is exposed via a .cmd / .bat shim (very common — claude.cmd, npm.cmd, gh.cmd, many others), asyncio.create_subprocess_exec() may pass argv into the shim in a form that causes the shim's cmd.exe interpreter to re-parse shell metacharacters embedded in user/agent-supplied arguments.

Symptoms range from broken behavior (e.g., |, >, &, ^, " in a prompt arg silently truncate or trigger pipes / redirects) to outright command injection if untrusted content reaches the argv.

Fix Action

Fix / Workaround

Workaround currently used downstream

Code Example

# Windows; assumes any .cmd shim on PATH (e.g., npm.cmd)
import asyncio

async def main():
    # The pipe in -e is meant to be a literal string, not a shell pipe.
    proc = await asyncio.create_subprocess_exec(
        "npm.cmd", "run", "echo", "--", "a | b",
        stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
    )
    out, err = await proc.communicate()
    print("stdout:", out.decode(errors="replace"))
    print("stderr:", err.decode(errors="replace"))

asyncio.run(main())
RAW_BUFFERClick to expand / collapse

Summary

On Windows, when Hermes spawns a CLI tool that is exposed via a .cmd / .bat shim (very common — claude.cmd, npm.cmd, gh.cmd, many others), asyncio.create_subprocess_exec() may pass argv into the shim in a form that causes the shim's cmd.exe interpreter to re-parse shell metacharacters embedded in user/agent-supplied arguments.

Symptoms range from broken behavior (e.g., |, >, &, ^, " in a prompt arg silently truncate or trigger pipes / redirects) to outright command injection if untrusted content reaches the argv.

Severity

  • Platform: Windows only
  • Affected sites: any code path that spawns a .cmd/.bat shim with user-supplied or LLM-supplied argv content. The most common case is claude / claude.exe resolving to claude.cmd via shutil.which().
  • Security angle: argv may contain Markdown, JSON, code snippets quoted from chat — all of which include |, &, >, <, ", ^ regularly.

Reproduction

# Windows; assumes any .cmd shim on PATH (e.g., npm.cmd)
import asyncio

async def main():
    # The pipe in -e is meant to be a literal string, not a shell pipe.
    proc = await asyncio.create_subprocess_exec(
        "npm.cmd", "run", "echo", "--", "a | b",
        stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
    )
    out, err = await proc.communicate()
    print("stdout:", out.decode(errors="replace"))
    print("stderr:", err.decode(errors="replace"))

asyncio.run(main())

On Linux/macOS this passes "a | b" to the script as a single literal argument. On Windows with a .cmd shim, cmd.exe's parser sees | and tries to start a pipeline → behavior diverges, often broken.

Workaround currently used downstream

In my own subprocess-executor work (#31385) I:

  1. Detect Windows + .cmd / .bat target via shutil.which() extension check
  2. Keep CLI argv short and static (no user-supplied content in argv)
  3. Send the actual prompt / content via stdin instead
  4. Apply tight input filter (-p-style args constrained to a short safelist)

This eliminates the parse-divergence completely, but it requires every spawn site to know about the .cmd quirk.

Proposed fix

Audit subprocess-spawn callsites in agent/ / gateway/ / tools/. For Windows + .cmd/.bat targets, route content via stdin or escape metacharacters consistently. Centralizing this in a small windows_safe_exec() helper would make sure every contributor gets it right by default.

Reference for the underlying CPython behavior: https://docs.python.org/3/library/subprocess.html#converting-an-argument-sequence-to-a-string-on-windows — note that the .cmd re-parse step happens after Python's argv-to-string conversion.

Happy to PR the audit + helper if there's interest. Filing as a bug for triage first.

Related: #31385 (executor bridge proposal where I hit this), #31417 (StreamReader 64 KiB bug).

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - 💡(How to fix) Fix [Bug] Windows: asyncio.subprocess_exec misparses argv metacharacters when target is a .cmd/.bat shim