Skip to content

fix(step): preserve cmd.exe quoting for {{files}} on Windows#824

Merged
jdx merged 4 commits intomainfrom
fix/windows-cmd-quoting
Apr 12, 2026
Merged

fix(step): preserve cmd.exe quoting for {{files}} on Windows#824
jdx merged 4 commits intomainfrom
fix/windows-cmd-quoting

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Apr 12, 2026

Fixes #823.

Summary

  • On Windows, Rust's Command::arg applies MSVCRT-style argv escaping that collides with cmd.exe's own quoting, so a rendered {{files}} payload (already quoted via ShellType::Cmd::quote) reaches tools with literal " characters in the arguments. In practice this silently broke ruff, biome, and any other {{files}}-based check — they'd exit 0 while processing zero files, so hk would report success on malformed invocations.
  • Switch the Windows cmd.exe /c path in src/step/runner.rs to use a new CmdLineRunner::raw_arg (paired with new_direct to avoid the implicit cmd.exe /c wrapping), so the rendered command string is appended verbatim via CommandExt::raw_arg and cmd.exe can parse its own quoting without Rust interference. Also covers {{workspace_files}}, which goes through the same shell-quoting path.
  • Requires ensembler PR: feat(cmd): add new_direct and raw_arg for Windows quoting ensembler#89 (submodule pointer bumped in this PR).

Test plan

  • cargo check (linux) passes.
  • cargo check --target x86_64-pc-windows-gnu passes.
  • Added a Pester regression test {{files}} template passes clean arguments on Windows (no literal quotes) in e2e-win/check.Tests.ps1: materializes files including hello world.txt, renders python check_args.py {{files}}, and fails if any arg contains a literal " or references a missing path.
  • Wired the new Pester test into the ci-windows job in .github/workflows/ci.yml.

🤖 Generated with Claude Code


Note

Medium Risk
Changes how step commands are executed on Windows cmd.exe by switching to a raw-argument path; this can affect argument parsing/escaping and could alter behavior for existing Windows hooks.

Overview
Fixes Windows cmd.exe execution so rendered {{files}}/{{workspace_files}} strings are passed verbatim to the shell (using CmdLineRunner::new_direct + raw_arg) instead of being re-escaped by Rust, preventing literal quote characters from reaching tools and silently dropping files.

Adds a Windows Pester regression test that validates {{files}} expands to real paths (including spaces) without embedded quotes, and wires that test into the ci-windows GitHub Actions job.

Reviewed by Cursor Bugbot for commit 6693338. Bugbot is set up for automated code reviews on this repo. Configure here.

Rust's `Command::arg` applies MSVCRT-style argv escaping on Windows,
which collides with cmd.exe's own parsing rules. When a step command
contains already-cmd-quoted paths (from rendering `{{files}}` or
`{{workspace_files}}` via `ShellType::Cmd::quote`), the resulting
argv re-escaping causes cmd.exe to deliver tools arguments with
literal `"` characters embedded — splitting "file with spaces.txt"
into three separate args and turning "simple.txt" into `"simple.txt"`.

In practice this silently broke `{{files}}`-based checks on Windows:
tools like ruff and biome would exit 0 while processing zero files,
so hk would report success on malformed invocations.

Use the new `raw_arg` helper (with `new_direct` to skip the implicit
`cmd.exe /c` wrapping) so the rendered command string is appended
verbatim via `CommandExt::raw_arg`, letting cmd.exe parse its own
quoting without Rust's interference.

Fixes #823.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request addresses an issue on Windows where Rust's default argument escaping conflicts with cmd.exe quoting, particularly when using the {{files}} template. The changes introduce the use of raw_arg for the default cmd.exe runner to bypass MSVCRT-style re-escaping and include a new regression test. Feedback suggests extending this logic to custom shells that are identified as cmd.exe on Windows to ensure the fix is applied consistently.

Comment thread src/step/runner.rs Outdated
cmd = cmd.arg(arg);
}
cmd
cmd.arg(&run)
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.

medium

The fix for cmd.exe quoting should also be applied when a custom shell is provided, if that shell is detected as cmd.exe. Currently, the custom shell block still uses .arg(&run), which will trigger the same MSVCRT-style escaping issue that this PR aims to fix for the default shell path. By using raw_arg when the shell type is Cmd on Windows, we ensure that the rendered command string (which already contains cmd.exe-appropriate quoting from ShellType::Cmd::quote) is passed through verbatim.

Suggested change
cmd.arg(&run)
if cfg!(windows) && self.shell_type() == ShellType::Cmd {
cmd.raw_arg(&run)
} else {
cmd.arg(&run)
}

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 12, 2026

Greptile Summary

Fixes the Windows cmd.exe double-escaping bug where Rust's MSVCRT argv escaping mangled {{files}}/{{workspace_files}} payloads that were already quoted by ShellType::Cmd::quote. The fix uses CmdLineRunner::new_direct + raw_arg so cmd.exe receives the verbatim command string and parses its own quoting correctly.

One unrelated file — .claude/scheduled_tasks.lock — was accidentally included in the commit and should be removed and gitignored.

Confidence Score: 5/5

Safe to merge; the core Windows fix and test coverage are correct, with only a trivial stale lock file to clean up.

All findings are P2. The runner.rs logic is sound — use_raw_cmd is correctly scoped to Windows+Cmd at both compile time and runtime, the sh path is structurally unchanged, and the ensembler submodule bump provides the new API. The Pester test now includes the positive file-presence assertions requested in the previous review thread. The only issue is an accidentally committed .claude/scheduled_tasks.lock that has no impact on hk's behavior.

.claude/scheduled_tasks.lock should be deleted and added to .gitignore; no other files need special attention.

Important Files Changed

Filename Overview
src/step/runner.rs Correctly gates Windows cmd.exe path on use_raw_cmd (compile-time cfg!(windows) + runtime ShellType::Cmd check); uses new_direct + raw_arg to avoid double-escaping of already-quoted {{files}} payloads; sh path is unchanged and structurally equivalent.
e2e-win/check.Tests.ps1 New Pester regression test covers the bug with a spaced filename and validates both negative (no literal quotes, no missing files) and positive (both files appear in output) assertions; addresses the gap flagged in the previous review thread.
.github/workflows/ci.yml Wires the new Pester test into ci-windows via -TestName "*{{files}} template*"; {{ is not a GitHub Actions expression (needs ${{), so the pattern passes through literally to Pester's wildcard filter.
.claude/scheduled_tasks.lock Accidentally committed Claude Code session lock file (session ID, PID, timestamp); should be deleted and added to .gitignore.
ensembler Submodule pointer bumped to provide the new CmdLineRunner::new_direct and raw_arg APIs required by the Windows fix.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Step::run] --> B{use_raw_cmd
cfg windows AND ShellType::Cmd}
    B -- Yes --> C{self.shell set?}
    B -- No --> D{self.shell set?}

    C -- Yes --> E["new_direct(shell[0])
+ arg(shell[1..])
+ raw_arg(&run)"]
    C -- No --> F["new_direct(cmd.exe)
+ arg(/c)
+ raw_arg(&run)"]

    D -- Yes --> G["new(shell[0])
+ arg(shell[1..])
+ arg(&run)"]
    D -- No --> H["new(sh)
+ arg(-o errexit -c)
+ arg(&run)"]

    E --> I[cmd.execute]
    F --> I
    G --> I
    H --> I

    style E fill:#d4edda,stroke:#28a745
    style F fill:#d4edda,stroke:#28a745
Loading

Fix All in Claude Code

Reviews (5): Last reviewed commit: "test(windows): use file:// URI for pkl a..." | Re-trigger Greptile

Comment thread e2e-win/check.Tests.ps1
Extend the Windows cmd.exe quoting fix to steps that explicitly set
`shell = "cmd.exe"` in hk.pkl. Previously the custom-shell branch
still used `CmdLineRunner::new` + `.arg(&run)`, which re-applied
MSVCRT escaping even though `ShellType::Cmd::quote` had already
produced cmd-appropriate quoting.

Also tighten the Pester regression test with a positive assertion on
both filenames, so a future regression that silently expands
`{{files}}` to an empty list still fails the test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jdx jdx force-pushed the fix/windows-cmd-quoting branch from b7d5a41 to 77de586 Compare April 12, 2026 12:40
The `{{files}}` Pester test on Windows exits 101 in CI with only the
bare Pester assertion message surfaced. Normalize `$env:PKL_PATH`
to forward slashes for the pkl amends URI, write files as ASCII (no
BOM), and dump the hk output + hk.pkl + dir listing on failure so the
next CI run reveals the underlying error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jdx jdx force-pushed the fix/windows-cmd-quoting branch from 77de586 to 4a4f6e4 Compare April 12, 2026 12:40
pkl's default `--allowed-modules` allowlist accepts `file:` URIs but
rejects bare Windows absolute paths like `D:/a/hk/...`, so the amends
`"D:/a/hk/hk/pkl/Config.pkl"` was causing hk to panic with a pkl
allowlist error. Build an explicit `file:///` URI from `$env:PKL_PATH`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jdx jdx force-pushed the fix/windows-cmd-quoting branch from 76d7a90 to 6693338 Compare April 12, 2026 12:53
@jdx jdx enabled auto-merge (squash) April 12, 2026 13:04
@jdx jdx merged commit 55e95f2 into main Apr 12, 2026
20 checks passed
@jdx jdx deleted the fix/windows-cmd-quoting branch April 12, 2026 13:11
@jdx jdx mentioned this pull request Apr 12, 2026
jdx added a commit that referenced this pull request Apr 12, 2026
### 🚀 Features

- add {{ hook_args }} template to pass to downstream commands by
[@JohanLorenzo](https://github.com/JohanLorenzo) in
[#807](#807)

### 🐛 Bug Fixes

- **(step)** preserve cmd.exe quoting for {{files}} on Windows by
[@jdx](https://github.com/jdx) in
[#824](#824)

### 🛡️ Security

- **(deps)** update dependency eslint to v10 by
[@renovate[bot]](https://github.com/renovate[bot]) in
[#813](#813)

### 📦️ Dependency Updates

- update taiki-e/upload-rust-binary-action digest to 10c1cf6 by
[@renovate[bot]](https://github.com/renovate[bot]) in
[#810](#810)
- update nick-fields/retry action to v4 by
[@renovate[bot]](https://github.com/renovate[bot]) in
[#812](#812)
- update anthropics/claude-code-action digest to 657fb7c by
[@renovate[bot]](https://github.com/renovate[bot]) in
[#809](#809)
- update dependency typescript to v6 by
[@renovate[bot]](https://github.com/renovate[bot]) in
[#814](#814)
- update rust crate demand to v2 by
[@renovate[bot]](https://github.com/renovate[bot]) in
[#815](#815)
- update rust crate expr-lang to v1 by
[@renovate[bot]](https://github.com/renovate[bot]) in
[#816](#816)
- update rust crate similar to v3 by
[@renovate[bot]](https://github.com/renovate[bot]) in
[#817](#817)
- update rust crate toml to v1 by
[@renovate[bot]](https://github.com/renovate[bot]) in
[#820](#820)
- update actions/upload-artifact digest to 043fb46 by
[@renovate[bot]](https://github.com/renovate[bot]) in
[#821](#821)
- update anthropics/claude-code-action digest to b47fd72 by
[@renovate[bot]](https://github.com/renovate[bot]) in
[#822](#822)

### New Contributors

- @JohanLorenzo made their first contribution in
[#807](#807)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk release bookkeeping: version bumps, regenerated CLI/docs, and
routine dependency lockfile updates with no functional code changes in
this PR.
> 
> **Overview**
> Bumps `hk` to **v1.42.0** and adds the corresponding `CHANGELOG.md`
release notes.
> 
> Regenerates versioned docs/CLI artifacts (`docs/cli/*`,
`hk.usage.kdl`) and updates all documentation/examples to reference the
`v1.42.0` Pkl package URLs. Updates `Cargo.lock` for minor dependency
revisions.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
4b9834f. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: mise-en-dev <123107610+mise-en-dev@users.noreply.github.com>
tmeijn pushed a commit to tmeijn/dotfiles that referenced this pull request Apr 15, 2026
This MR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [hk](https://github.com/jdx/hk) | minor | `1.41.1` → `1.42.0` |

MR created with the help of [el-capitano/tools/renovate-bot](https://gitlab.com/el-capitano/tools/renovate-bot).

**Proposed changes to behavior should be submitted there as MRs.**

---

### Release Notes

<details>
<summary>jdx/hk (hk)</summary>

### [`v1.42.0`](https://github.com/jdx/hk/blob/HEAD/CHANGELOG.md#1420---2026-04-12)

[Compare Source](jdx/hk@v1.41.1...v1.42.0)

##### 🚀 Features

- add {{ hook\_args }} template to pass to downstream commands by [@&#8203;JohanLorenzo](https://github.com/JohanLorenzo) in [#&#8203;807](jdx/hk#807)

##### 🐛 Bug Fixes

- **(step)** preserve cmd.exe quoting for {{files}} on Windows by [@&#8203;jdx](https://github.com/jdx) in [#&#8203;824](jdx/hk#824)

##### 🛡️ Security

- **(deps)** update dependency eslint to v10 by [@&#8203;renovate\[bot\]](https://github.com/renovate\[bot]) in [#&#8203;813](jdx/hk#813)

##### 📦️ Dependency Updates

- update taiki-e/upload-rust-binary-action digest to [`10c1cf6`](jdx/hk@10c1cf6) by [@&#8203;renovate\[bot\]](https://github.com/renovate\[bot]) in [#&#8203;810](jdx/hk#810)
- update nick-fields/retry action to v4 by [@&#8203;renovate\[bot\]](https://github.com/renovate\[bot]) in [#&#8203;812](jdx/hk#812)
- update anthropics/claude-code-action digest to [`657fb7c`](jdx/hk@657fb7c) by [@&#8203;renovate\[bot\]](https://github.com/renovate\[bot]) in [#&#8203;809](jdx/hk#809)
- update dependency typescript to v6 by [@&#8203;renovate\[bot\]](https://github.com/renovate\[bot]) in [#&#8203;814](jdx/hk#814)
- update rust crate demand to v2 by [@&#8203;renovate\[bot\]](https://github.com/renovate\[bot]) in [#&#8203;815](jdx/hk#815)
- update rust crate expr-lang to v1 by [@&#8203;renovate\[bot\]](https://github.com/renovate\[bot]) in [#&#8203;816](jdx/hk#816)
- update rust crate similar to v3 by [@&#8203;renovate\[bot\]](https://github.com/renovate\[bot]) in [#&#8203;817](jdx/hk#817)
- update rust crate toml to v1 by [@&#8203;renovate\[bot\]](https://github.com/renovate\[bot]) in [#&#8203;820](jdx/hk#820)
- update actions/upload-artifact digest to [`043fb46`](jdx/hk@043fb46) by [@&#8203;renovate\[bot\]](https://github.com/renovate\[bot]) in [#&#8203;821](jdx/hk#821)
- update anthropics/claude-code-action digest to [`b47fd72`](jdx/hk@b47fd72) by [@&#8203;renovate\[bot\]](https://github.com/renovate\[bot]) in [#&#8203;822](jdx/hk#822)

##### New Contributors

- [@&#8203;JohanLorenzo](https://github.com/JohanLorenzo) made their first contribution in [#&#8203;807](jdx/hk#807)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this MR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box

---

This MR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMTAuMTMiLCJ1cGRhdGVkSW5WZXIiOiI0My4xMTAuMTMiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbIlJlbm92YXRlIEJvdCIsImF1dG9tYXRpb246Ym90LWF1dGhvcmVkIiwiZGVwZW5kZW5jeS10eXBlOjptaW5vciJdfQ==-->
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.

1 participant