Skip to content

Fix Windows hook dispatch via explicit bash invocation#114

Open
ianliuy wants to merge 1 commit intoPolyArch:mainfrom
ianliuy:pr/windows-fix
Open

Fix Windows hook dispatch via explicit bash invocation#114
ianliuy wants to merge 1 commit intoPolyArch:mainfrom
ianliuy:pr/windows-fix

Conversation

@ianliuy
Copy link
Copy Markdown

@ianliuy ianliuy commented Apr 26, 2026

Problem

GitHub Copilot CLI on Windows installs plugin hooks under ~/.copilot/installed-plugins/<owner>/<plugin>/hooks/ and dispatches each hooks.json entry's command field through pwsh. With the existing bare-path form (${CLAUDE_PLUGIN_ROOT}/hooks/<name>.sh), Windows' OS-level dispatch hands the .sh file to the registered editor (typically VS Code) instead of executing it, because Windows has no native handler for the .sh extension. 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 command field from a bare path to an explicit bash "<path>" invocation:

- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-bash-validator.sh"
+ "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/loop-bash-validator.sh\""

On hosts that shell-execute the command field, this resolves bash via PATH (Git for Windows' bash.exe 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, so the new form converges on the same dispatch chain on POSIX hosts. The Windows symptom goes away because the .sh path is now an argument to bash, 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 the command value itself.

Also: backslash normalization in is_in_humanize_loop_dir

hooks/lib/loop-common.sh::is_in_humanize_loop_dir greps for the literal .humanize/rlcr/ (POSIX forward slashes), but Claude Code on Windows passes tool_input.file_path to hooks in native backslash form (C:\...\.humanize\rlcr\...). The grep silently never matches, and loop-write-validator.sh then 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

Host How verified Result
Windows Copilot CLI v1.0.36-1 Full Humanize /humanize:gen-plan + /humanize:start-rlcr-loop workflow under autopilot mode (multi-phase, parallel sub-agents, dozens of hook firings) ✅ All hooks fire silently, no .sh tabs
Windows Claude Code PreToolUse Write deliberately given an out-of-loop contract path; observed loop-write-validator.sh emit "Wrong Round Contract Location" ✅ Hook actually executes (proves bash invokes the .sh body, not silent failure)
macOS / Linux Claude Code Same dispatch model as Windows (Anthropic ships one cross-platform binary; the OS-conditional layer is process spawn, not hook loading) Inherited

Scope

  • 5 files touched, 14 insertions / 12 deletions
  • No new dependencies
  • Existing test suite continues to pass (the change is in the command string 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 .cmd launcher files, a commands/*.md allowed-tools dual-spelling, a Windows CI workflow for the launchers, and supporting docs. Those were based on the now-falsified windows:-override hypothesis and are unnecessary with the bash-prefix approach. They're not included here.

Version bump

1.15.4 → 1.15.5 across .claude-plugin/plugin.json, .claude-plugin/marketplace.json, and README.md per the main-branch version-bump rule from .claude/CLAUDE.md.

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.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment thread hooks/lib/loop-common.sh
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/'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

@SihaoLiu
Copy link
Copy Markdown
Contributor

SihaoLiu commented Apr 26, 2026

Can you try to re-target to beta branch?

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