Fix Windows hook dispatch via explicit bash invocation#114
Fix Windows hook dispatch via explicit bash invocation#114ianliuy wants to merge 1 commit intoPolyArch:mainfrom
Conversation
GitHub Copilot CLI on Windows installs plugin hooks under
~/.copilot/installed-plugins/<owner>/<plugin>/hooks/ and dispatches the
"command" string of each hooks.json entry through pwsh. With the bare-path
form ("${CLAUDE_PLUGIN_ROOT}/hooks/<name>.sh"), Windows' OS-level dispatch
hands the .sh to the registered editor (typically VS Code) instead of
executing it, because Windows has no native handler for the .sh extension.
The visible symptom is that every plugin hook source file pops open as a
tab in the editor at session start and on each tool use.
Switch each hook entry from a bare path to "bash <path>". On hosts that
shell-execute the command field, this resolves bash via PATH (Git for
Windows on Windows; system bash on macOS/Linux) and runs the script
explicitly. The change is backwards-compatible: macOS/Linux Claude Code
already shell-executed the bare-path form via the kernel's shebang
resolution to /bin/bash; making the invocation explicit removes one layer
of OS-level dispatch ambiguity without changing the effective dispatch
chain on POSIX hosts.
Verified empirically:
- Windows Copilot CLI v1.0.36-1: full Humanize gen-plan + start-rlcr-loop
workflow runs end-to-end under autopilot mode with no .sh tabs popping
up; PreToolUse, PostToolUse, and Stop hooks all fire silently.
- Windows Claude Code: PreToolUse Write hook fires correctly and emits
"Wrong Round Contract Location" when given an out-of-loop contract path,
proving bash actually executes the .sh body (not silent failure).
Also fix hooks/lib/loop-common.sh::is_in_humanize_loop_dir to normalize
backslashes to forward slashes before grepping for ".humanize/rlcr/".
Without this, every Windows-form path (C:\...\.humanize\rlcr\...) that
Claude Code's Write tool passes to the hook fails the substring match,
which makes the write-validator misclassify any in-loop contract or
summary write as outside the loop directory and reject it with "Wrong
Round Contract Location" even when the path is correct. The fix is a
one-line bash parameter expansion that is a no-op on POSIX hosts.
Version bump per the main-branch version-bump rule.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8927759f2c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| echo "$path" | grep -q '\.humanize/rlcr/' | ||
| # Normalize backslashes to forward slashes so Windows paths (C:\...\.humanize\rlcr\...) | ||
| # match the POSIX pattern the rest of the hook tree expects. | ||
| echo "${path//\\//}" | grep -q '\.humanize/rlcr/' |
There was a problem hiding this comment.
Preserve round-number enforcement for Windows paths
This change makes is_in_humanize_loop_dir return true for backslash-separated Windows paths, but the downstream Write/Edit validators still parse loop-relative filenames with forward-slash-only sed patterns (hooks/loop-write-validator.sh:218-223 and hooks/loop-edit-validator.sh:157-160). For inputs like C:\...\.humanize\rlcr\...\round-99-summary.md, filename extraction becomes empty and both validators fall through to exit 0, allowing wrong-round summary writes/edits that should be blocked. I verified the behavior change by executing the parent commit and this commit with the same Windows-style path: parent returns exit 2 (blocked), this commit returns exit 0 (allowed).
Useful? React with 👍 / 👎.
|
Can you try to re-target to |
Problem
GitHub Copilot CLI on Windows installs plugin hooks under
~/.copilot/installed-plugins/<owner>/<plugin>/hooks/and dispatches eachhooks.jsonentry'scommandfield through pwsh. With the existing bare-path form (${CLAUDE_PLUGIN_ROOT}/hooks/<name>.sh), Windows' OS-level dispatch hands the.shfile to the registered editor (typically VS Code) instead of executing it, because Windows has no native handler for the.shextension. The visible symptom: every Humanize hook source file pops open as a tab in the editor at session start and on each tool use, and hooks never actually fire.This makes Humanize unusable under Copilot CLI on Windows, even though all the underlying script logic is host-agnostic.
Fix
Switch each hook entry's
commandfield from a bare path to an explicitbash "<path>"invocation:On hosts that shell-execute the
commandfield, this resolvesbashviaPATH(Git for Windows'bash.exeon Windows; system bash on macOS/Linux) and runs the script explicitly. The change is backwards-compatible: macOS/Linux Claude Code already shell-executed the bare-path form via the kernel's shebang resolution to/bin/bash, so the new form converges on the same dispatch chain on POSIX hosts. The Windows symptom goes away because the.shpath is now an argument tobash, not a thing dispatched via Windows file association.Why not the
windows:platform-override field?An earlier iteration of this work tried adding a
"windows": "<path>.cmd"platform-override field to each hook entry, based on a reading of the VS Code Agent Hooks documentation which describes that field. Empirical testing on Copilot CLI v1.0.36-1 showed Copilot CLI does not honor that field for plugin-loaded hooks: it only supports Claude Code's nested matcher/hooks structure (per github/copilot-cli changelog 1.0.6), which has no platform-override fields. The VS Code Agent Hooks docs describe VS Code's editor-side local hooks, not Copilot CLI's plugin-hook loader. So the fix needs to live in thecommandvalue itself.Also: backslash normalization in
is_in_humanize_loop_dirhooks/lib/loop-common.sh::is_in_humanize_loop_dirgreps for the literal.humanize/rlcr/(POSIX forward slashes), but Claude Code on Windows passestool_input.file_pathto hooks in native backslash form (C:\...\.humanize\rlcr\...). The grep silently never matches, andloop-write-validator.shthen misclassifies any in-loop contract or summary write as "outside the loop directory" and rejects it with "Wrong Round Contract Location" — even when the path is correct.One-line fix: normalize backslashes before grepping.
is_in_humanize_loop_dir() { local path="$1" - echo "$path" | grep -q '\.humanize/rlcr/' + # Normalize backslashes to forward slashes so Windows paths + # (C:\...\.humanize\rlcr\...) match the POSIX pattern. + echo "${path//\//}" | grep -q '\.humanize/rlcr/' }This is a no-op on POSIX hosts (where paths have no backslashes).
Empirical verification
/humanize:gen-plan+/humanize:start-rlcr-loopworkflow under autopilot mode (multi-phase, parallel sub-agents, dozens of hook firings).shtabsPreToolUse Writedeliberately given an out-of-loop contract path; observedloop-write-validator.shemit "Wrong Round Contract Location"Scope
commandstring only, not in any hook script body)What's NOT in this PR
This PR is the minimum viable fix. Earlier exploration in the fork tree included
.cmdlauncher files, acommands/*.mdallowed-tools dual-spelling, a Windows CI workflow for the launchers, and supporting docs. Those were based on the now-falsifiedwindows:-override hypothesis and are unnecessary with the bash-prefix approach. They're not included here.Version bump
1.15.4 → 1.15.5across.claude-plugin/plugin.json,.claude-plugin/marketplace.json, andREADME.mdper the main-branch version-bump rule from.claude/CLAUDE.md.