Skip to content

fix(task): preserve inner quotes for cmd /c tasks and hooks on windows#10301

Merged
jdx merged 1 commit into
jdx:mainfrom
JamBalaya56562:fix/windows-cmd-c-inner-quotes
Jun 11, 2026
Merged

fix(task): preserve inner quotes for cmd /c tasks and hooks on windows#10301
jdx merged 1 commit into
jdx:mainfrom
JamBalaya56562:fix/windows-cmd-c-inner-quotes

Conversation

@JamBalaya56562

@JamBalaya56562 JamBalaya56562 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Problem

On Windows, inner double quotes in a task's run string — and in [hooks] commands — are mangled when spawned through the default cmd /c shell. Reported in #9355 (originally #6119).

[tasks.repro]
run = 'uv run --no-project python -c "import pathlib; print(pathlib.Path.home())"'
DEBUG $ cmd /c uv run --no-project python -c "import pathlib; print(pathlib.Path.home())"
  File "<string>", line 1
    "import
SyntaxError: unterminated string literal

The debug line looks correct, but Python receives just "import. Likewise run = 'echo "x"' prints \"x\". [hooks] (postinstall, enter, cd, …) are hit identically, and unlike tasks they have no usage/shebang/file-based escape hatch.

Root cause

mise builds the argv ["cmd", "/c", <script>, …] and passes the script as an ordinary std::process::Command argument. On Windows, std serializes argv into lpCommandLine using MSVCRT/CommandLineToArgvW-style quoting (it wraps the script in quotes and escapes inner " as \"). cmd.exe does not parse that convention — it treats \" literally — so the child receives mangled arguments. No raw_arg was used anywhere in the tree.

Fix

When the inline/hook shell is cmd.exe, build the command line verbatim via raw command-line args:

  • The whole command is wrapped in a single outer quote pair and run as cmd /s /c "<body>". /s strips exactly that one outer pair and leaves the remainder — including the user's inner quotes — untouched.
  • Forwarded args are MSVCRT-quoted inside the outer pair so the program still sees them as separate arguments, preserving the spaces-in-args fix from fix: incorrect task arguments with spaces on Windows #6744 (e.g. mise run type ".\test dir\file.txt").
  • [hooks] spawn through duct, which can't emit raw args, so the cmd case now spawns a std::process::Command directly and forwards stdout to stderr to match the previous stdout_to_stderr() behavior.
  • Non-cmd shells (pwsh -Command, bash -c, …) are gated out via is_cmd_shell_program and keep the existing behavior.

New helpers live in src/path.rs (is_cmd_shell_program, cmd_verbatim_args, quote_arg_for_cmd_body) so the other call sites can reuse them later.

Scope / follow-ups

  • This PR fixes inline tasks and [hooks] (the two paths reported in mise tasks on Windows arguments with spaces not being escaped? #9355).
  • The same root cause also affects watch_files, deps, {{ exec() }}, mise exec -c, and backend scripts. Those can reuse cmd_verbatim_args in a follow-up.
  • The separate shell = "bash -c" $0-shift issue noted in the discussion is left for a follow-up.

Testing

  • Unit tests for is_cmd_shell_program, cmd_verbatim_args, and quote_arg_for_cmd_body (covering the MSVCRT quoting edge cases and the type ".\test dir\file.txt" case from e2e-win/task_args.Tests.ps1). I verified this pure string logic locally.
  • New e2e-win/task_quoting.Tests.ps1: asserts inner quotes survive (no \ mangling) and that an inner-quoted -c argument reaches the program as a single argument.
  • Heads-up: my local environment couldn't compile the full crate (a C-dependency toolchain limitation) and can't compile the #[cfg(windows)] branches at all, so I'm relying on CI for the Windows build, clippy, and the e2e-win run.

Refs #9355, #6119.


This pull request was prepared with the help of an AI coding assistant.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed Windows task and hook execution so inline commands run via cmd preserve inner double quotes and don’t produce stray backslashes, improving reliability of quoted arguments.
  • Tests

    • Added a Windows end-to-end test that validates quoted-string preservation across task runs (skips if Node is unavailable).

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bc497b32-19b7-4be4-af6b-21f8ad45af7b

📥 Commits

Reviewing files that changed from the base of the PR and between 02bf972 and 757123f.

📒 Files selected for processing (5)
  • e2e-win/task_quoting.Tests.ps1
  • src/cmd.rs
  • src/hooks.rs
  • src/path.rs
  • src/task/task_executor.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/cmd.rs
  • src/hooks.rs
  • src/path.rs
  • src/task/task_executor.rs

📝 Walkthrough

Walkthrough

This PR adds Windows-specific support for preserving inner double quotes when executing inline scripts and hooks via cmd.exe /c. It introduces path helpers for cmd detection and verbatim argument construction, extends CmdLineRunner with a raw_arg method, and threads a cmd_verbatim flag through task and hook execution layers to bypass stdlib quoting when appropriate.

Changes

Windows cmd.exe Quote Preservation

Layer / File(s) Summary
Path detection and verbatim argument construction
src/path.rs
is_cmd_shell_program identifies cmd/cmd.exe. cmd_verbatim_args builds a single outer-quoted command body and MSVCRT-quotes forwarded args via quote_arg_for_cmd_body. Unit tests cover detection, /s deduplication, inner quoting, and backslash handling.
CmdLineRunner raw_arg method
src/cmd.rs
Windows-only raw_arg on CmdLineRunner forwards a single argument verbatim via CommandExt::raw_arg.
Hook execution with cmd.exe verbatim path
src/hooks.rs
Windows-only execute() branch detects cmd.exe with /c or /k, spawns cmd.exe with verbatim args (clearing/applying env and piping stdout to stderr), and checks status.
Task executor cmd_verbatim threading
src/task/task_executor.rs
get_cmd_program_and_args returns (program, args, cmd_verbatim) and marks cmd.exe /c//k cases. exec_script/exec_program thread the flag; on Windows exec_program uses raw_arg when cmd_verbatim is true, otherwise uses normal arg appending.
Windows e2e task quoting tests
e2e-win/task_quoting.Tests.ps1
PowerShell e2e tests write a mise.toml with quoted runs, run tasks, and assert inner quotes are preserved and no stray backslashes appear; includes a Node regression test that skips when node is absent.

Sequence Diagram

sequenceDiagram
  participant TaskExecutor
  participant get_cmd_program_and_args
  participant exec_program
  participant CmdLineRunner
  participant Command

  TaskExecutor->>get_cmd_program_and_args: script, shell=/c
  get_cmd_program_and_args->>get_cmd_program_and_args: is_cmd_shell_program?
  get_cmd_program_and_args->>get_cmd_program_and_args: cmd_verbatim_args(flags, script, forwarded_args)
  get_cmd_program_and_args-->>TaskExecutor: (program, args, cmd_verbatim=true)
  TaskExecutor->>exec_program: program, args, cmd_verbatim=true
  exec_program->>CmdLineRunner: new(program)
  alt Windows & cmd_verbatim
    CmdLineRunner->>CmdLineRunner: raw_arg(args[i]) for each arg
    CmdLineRunner->>Command: raw args passed unchanged
  else Normal path
    CmdLineRunner->>CmdLineRunner: args(args)
    CmdLineRunner->>Command: stdlib/MSVCRT quoting applied
  end
  Command-->>CmdLineRunner: spawned
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • jdx/mise#10202: Modifies Windows hook execution paths that interact with how execute() constructs and spawns cmd quoting logic.

Poem

🐰 I nibble strings in cmd.exe fields,
Inner quotes safe as my tiny shields,
Raw args march through without a fuss,
Tests hop by to cheer for us,
A little rabbit dance for preserved yields.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely summarizes the main change: fixing a Windows-specific issue where inner quotes in cmd /c task and hook execution were being mangled, and the fix preserves those quotes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes inner-double-quote mangling in Windows cmd /c tasks and hooks by building the command line verbatim — wrapping the whole body in a single outer "..." pair and using cmd /s /c to strip exactly that pair — instead of going through std's MSVCRT-style quoting, which escapes " as \" in a way cmd.exe does not understand.

  • src/path.rs: Adds is_cmd_shell_program, cmd_verbatim_args, and pub(crate) quote_arg_for_cmd_body. The MSVCRT quoting in quote_arg_for_cmd_body (backslash-doubling before ", trailing-backslash doubling) correctly encodes forwarded args so the child C runtime sees them as separate arguments while cmd metacharacters are suppressed inside the quoted span.
  • src/task/task_executor.rs: get_cmd_program_and_args now returns a third cmd_verbatim flag; exec_program uses CmdLineRunner::raw_arg on Windows when the flag is set, bypassing std's re-quoting.
  • src/hooks.rs: The hook execute path spawns a bare std::process::Command with raw_arg on Windows+cmd, forwarding stdout to a cloned stderr handle — matching the existing duct stdout_to_stderr() behaviour without introducing a pipe that a background grandchild could hold open.

Confidence Score: 5/5

Safe to merge; all changes are Windows-only, the quoting algorithm is correct, and non-Windows paths are untouched.

The fix is narrowly scoped to Windows cmd.exe inline tasks and hooks. The backslash-doubling algorithm in quote_arg_for_cmd_body matches the MSVCRT spec exactly, the cmd /s /c outer-pair-stripping approach is a well-known cmd idiom, and the three code paths (task executor, hooks, cmd runner) are each consistent with their existing patterns. Unit tests cover the key quoting edge cases; the e2e test covers the end-to-end regression scenario on CI.

No files require special attention.

Important Files Changed

Filename Overview
src/path.rs Adds is_cmd_shell_program, cmd_verbatim_args, and quote_arg_for_cmd_body helpers; algorithm is correct (backslash doubling, trailing-backslash doubling, metacharacter quoting); comprehensive unit tests added
src/task/task_executor.rs get_cmd_program_and_args now returns a cmd_verbatim bool; exec_program uses raw_arg on Windows when true; non-Windows path unchanged; logic is correct
src/hooks.rs Windows cmd path added: spawns std::process::Command with raw args, clones stderr handle for stdout redirect, env_clear + envs matches existing duct behavior
src/cmd.rs Adds CmdLineRunner::raw_arg on Windows only; correctly delegates to std CommandExt::raw_arg
e2e-win/task_quoting.Tests.ps1 New e2e test covering echo with inner quotes, quoted filename with spaces, and inner-quoted -c arg; node skip guard is correct

Reviews (2): Last reviewed commit: "fix(task): preserve inner quotes for cmd..." | Re-trigger Greptile

Comment thread src/path.rs
Comment thread src/hooks.rs Outdated
@JamBalaya56562 JamBalaya56562 marked this pull request as ready for review June 11, 2026 05:51

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e-win/task_quoting.Tests.ps1`:
- Around line 11-30: The AfterAll currently deletes the environment variable set
in BeforeAll; instead capture the original value of
$env:MISE_TRUSTED_CONFIG_PATHS in BeforeAll (e.g. $originalMISETrusted =
$env:MISE_TRUSTED_CONFIG_PATHS) and in AfterAll restore it (set
$env:MISE_TRUSTED_CONFIG_PATHS = $originalMISETrusted) or remove it only if it
did not exist originally, modifying the BeforeAll/AfterAll blocks and references
to $env:MISE_TRUSTED_CONFIG_PATHS and $originalPath accordingly.

In `@src/hooks.rs`:
- Around line 611-621: The current change pipes the child's stdout and spawns a
copier thread (variables c, child, copier) which can deadlock or reorder output
compared to the previous stdout_to_stderr() behavior; restore the old semantics
by not piping stdout for the child but redirecting the child's stdout to stderr
before spawn (or call the original stdout_to_stderr() helper) so background
children inherit the parent's descriptors and we avoid a blocking join on copier
and EOF waits; replace c.stdout(std::process::Stdio::piped()) + copier logic
with the prior redirection approach (e.g., use the stdout_to_stderr() path or
set c.stdout(...) to inherit / dup the fd to stderr) and remove the
join/wait-on-copier logic so child.wait() cannot block forever.

In `@src/path.rs`:
- Around line 205-208: quote_arg_for_cmd_body currently only quotes args with
whitespace or double quotes, leaving Windows cmd metacharacters (e.g. & | < > ^
% () etc.) unquoted so they get interpreted by cmd.exe; update
quote_arg_for_cmd_body to also detect any of the cmd metacharacters and treat
them like whitespace (i.e. wrap the argument in quotes) and ensure any internal
" characters are properly escaped/handled for the cmd /s /c "... " form; locate
the function quote_arg_for_cmd_body and change the initial if check to include a
test for these metacharacters and then return a safely quoted/escaped string
instead of the bare arg.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5188fc94-fd5c-4c03-b62d-8aef69ca3445

📥 Commits

Reviewing files that changed from the base of the PR and between ecc7213 and af8d7b7.

📒 Files selected for processing (5)
  • e2e-win/task_quoting.Tests.ps1
  • src/cmd.rs
  • src/hooks.rs
  • src/path.rs
  • src/task/task_executor.rs

Comment thread e2e-win/task_quoting.Tests.ps1 Outdated
Comment thread src/hooks.rs Outdated
Comment thread src/path.rs Outdated
@JamBalaya56562 JamBalaya56562 force-pushed the fix/windows-cmd-c-inner-quotes branch from af8d7b7 to 02bf972 Compare June 11, 2026 07:29
On Windows, inner double quotes in a task's `run` string (and in `[hooks]`
commands) were mangled when spawned through the default `cmd /c` shell. mise
passed the script as an ordinary argument to std::process::Command, which
serializes argv into lpCommandLine using MSVCRT-style quoting (escaping inner
`"` as `\"`). cmd.exe does not understand that escaping, so a command like
`uv run python -c "import x"` reached the child as `\"import` and failed with a
syntax error. `[hooks]` were hit identically, with no usage/shebang/file-based
workaround available.

Fix: when the inline/hook shell is cmd.exe, build the command line verbatim via
raw command-line args. The whole command is wrapped in a single outer quote
pair and run with `cmd /s /c "..."`, so cmd strips exactly that pair and leaves
the user's inner quotes intact. Forwarded args are MSVCRT-quoted inside the
outer pair so the spaces-in-args fix from jdx#6744 is preserved.

`[hooks]` spawn through duct, which can't emit raw args, so the cmd case now
spawns a std Command directly and forwards stdout to stderr to match the
previous `stdout_to_stderr()` behavior. Adds `is_cmd_shell_program` /
`cmd_verbatim_args` (+ unit tests) and an e2e-win test asserting inner quotes
survive.

Refs jdx#9355 and jdx#6119.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@JamBalaya56562 JamBalaya56562 force-pushed the fix/windows-cmd-c-inner-quotes branch from 02bf972 to 757123f Compare June 11, 2026 07:36
@jdx jdx merged commit 0f11fe3 into jdx:main Jun 11, 2026
33 checks passed
@JamBalaya56562 JamBalaya56562 deleted the fix/windows-cmd-c-inner-quotes branch June 11, 2026 23:26
JamBalaya56562 added a commit to JamBalaya56562/mise that referenced this pull request Jun 12, 2026
…es on windows

jdx#10301 fixed inner-quote mangling for inline tasks and [hooks] on Windows by
passing the command to cmd verbatim (`cmd /s /c "<body>"`). The same root cause
affects the other call sites that spawn `cmd /c <command>`: `mise exec -c`, the
tera `exec()` template function, [[watch_files]] hooks, tool postinstall hooks,
[deps] commands, and credential commands.

Add two shared helpers that reuse jdx#10301's `cmd_verbatim_args`:
- `path::cmd_verbatim_command(program, flags, body) -> Option<Command>` for the
  duct / raw-Command sites (returns None for non-cmd shells so callers fall
  through to their existing path unchanged).
- `CmdLineRunner::cmd_body_args(flags, body)` for the CmdLineRunner sites
  (identical to `args(flags).arg(body)` on Unix / non-cmd shells).

Wire them into exec.rs (windows variant), tera.rs, watch_files.rs,
backend/mod.rs, deps/engine.rs, and tokens.rs. Windows-only behavior change;
non-cmd shells and all Unix paths are byte-for-byte unchanged.

Adds unit gate tests for both helpers plus Unix and Windows e2e coverage
(e2e/env/test_env_template, e2e-win/exec_inner_quotes.Tests.ps1).

Refs jdx#9355.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JamBalaya56562 added a commit to JamBalaya56562/mise that referenced this pull request Jun 12, 2026
…es on windows

jdx#10301 fixed inner-quote mangling for inline tasks and [hooks] on Windows by
passing the command to cmd verbatim (`cmd /s /c "<body>"`). The same root cause
affects the other call sites that spawn `cmd /c <command>`: `mise exec -c`, the
tera `exec()` template function, [[watch_files]] hooks, tool postinstall hooks,
[deps] commands, and credential commands.

Add two shared helpers that reuse jdx#10301's `cmd_verbatim_args`:
- `path::cmd_verbatim_command(program, flags, body) -> Option<Command>` for the
  duct / raw-Command sites (returns None for non-cmd shells so callers fall
  through to their existing path unchanged).
- `CmdLineRunner::cmd_body_args(flags, body)` for the CmdLineRunner sites
  (identical to `args(flags).arg(body)` on Unix / non-cmd shells).

Wire them into exec.rs (windows variant), tera.rs, watch_files.rs,
backend/mod.rs, deps/engine.rs, and tokens.rs. Windows-only behavior change;
non-cmd shells and all Unix paths are byte-for-byte unchanged.

Adds unit gate tests for both helpers plus Unix and Windows e2e coverage
(e2e/env/test_env_template, e2e-win/exec_inner_quotes.Tests.ps1).

Refs jdx#9355.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JamBalaya56562 added a commit to JamBalaya56562/mise that referenced this pull request Jun 12, 2026
…es on windows

jdx#10301 fixed inner-quote mangling for inline tasks and [hooks] on Windows by
passing the command to cmd verbatim (`cmd /s /c "<body>"`). The same root cause
affects the other call sites that spawn `cmd /c <command>`: `mise exec -c`, the
tera `exec()` template function, [[watch_files]] hooks, tool postinstall hooks,
[deps] commands, and credential commands.

Add two shared helpers that reuse jdx#10301's `cmd_verbatim_args`:
- `path::cmd_verbatim_command(program, flags, body) -> Option<Command>` for the
  duct / raw-Command sites (returns None for non-cmd shells so callers fall
  through to their existing path unchanged).
- `CmdLineRunner::cmd_body_args(flags, body)` for the CmdLineRunner sites
  (identical to `args(flags).arg(body)` on Unix / non-cmd shells).

Wire them into exec.rs (windows variant), tera.rs, watch_files.rs,
backend/mod.rs, deps/engine.rs, and tokens.rs. Windows-only behavior change;
non-cmd shells and all Unix paths are byte-for-byte unchanged.

Adds unit gate tests for both helpers plus Unix and Windows e2e coverage
(e2e/env/test_env_template, e2e-win/exec_inner_quotes.Tests.ps1).

Refs jdx#9355.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JamBalaya56562 added a commit to JamBalaya56562/mise that referenced this pull request Jun 12, 2026
…es on windows

jdx#10301 fixed inner-quote mangling for inline tasks and [hooks] on Windows by
passing the command to cmd verbatim (`cmd /s /c "<body>"`). The same root cause
affects the other call sites that spawn `cmd /c <command>`: `mise exec -c`, the
tera `exec()` template function, [[watch_files]] hooks, tool postinstall hooks,
[deps] commands, and credential commands.

Add two shared helpers that reuse jdx#10301's `cmd_verbatim_args`:
- `path::cmd_verbatim_command(program, flags, body) -> Option<Command>` for the
  duct / raw-Command sites (returns None for non-cmd shells so callers fall
  through to their existing path unchanged).
- `CmdLineRunner::cmd_body_args(flags, body)` for the CmdLineRunner sites
  (identical to `args(flags).arg(body)` on Unix / non-cmd shells).

Wire them into exec.rs (windows variant), tera.rs, watch_files.rs,
backend/mod.rs, deps/engine.rs, and tokens.rs. Windows-only behavior change;
non-cmd shells and all Unix paths are byte-for-byte unchanged.

Adds unit gate tests for both helpers plus Unix and Windows e2e coverage
(e2e/env/test_env_template, e2e-win/exec_inner_quotes.Tests.ps1).

Refs jdx#9355.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants