| 200 | |
| 201 | |
| 202 | async def _execute_command_hook( |
| 203 | hook: HookConfig, |
| 204 | stdin_data: dict[str, Any], |
| 205 | abort_signal: Any | None = None, |
| 206 | timeout_ms: int = TOOL_HOOK_EXECUTION_TIMEOUT_MS, |
| 207 | tool_use_context: Any | None = None, |
| 208 | ) -> HookResult: |
| 209 | command = hook.command |
| 210 | if not command: |
| 211 | return HookResult() |
| 212 | |
| 213 | effective_timeout = (hook.timeout or timeout_ms) / 1000.0 |
| 214 | start_time = time.monotonic() |
| 215 | |
| 216 | try: |
| 217 | stdin_json = json.dumps(stdin_data, default=str) |
| 218 | |
| 219 | # Round-2 / Ch12 — per-hook shell selection. ``shell="powershell"`` |
| 220 | # spawns ``pwsh`` with explicit argv and skips the bash-shell path. |
| 221 | # ``None`` / ``"bash"`` keeps the historical ``create_subprocess_shell`` |
| 222 | # invocation. Mirrors the TS branch at |
| 223 | # ``typescript/src/utils/hooks.ts:1098-1125``. |
| 224 | if hook.shell == "powershell": |
| 225 | from .shell_invocation import build_powershell_args, find_powershell_path |
| 226 | |
| 227 | pwsh_path = find_powershell_path() |
| 228 | if pwsh_path is None: |
| 229 | duration_ms = int((time.monotonic() - start_time) * 1000) |
| 230 | # Error string mirrors TS at typescript/src/utils/hooks.ts:1102-1106 |
| 231 | # verbatim (single quotes around 'powershell' as in the TS source) |
| 232 | # so log scrapers / regression tests written against TS messages |
| 233 | # transfer unchanged. |
| 234 | return HookResult( |
| 235 | blocking_error=( |
| 236 | f"Hook \"{command}\" has shell: 'powershell' but no " |
| 237 | "PowerShell executable (pwsh or powershell) was found " |
| 238 | "on PATH. Install PowerShell, or remove " |
| 239 | "\"shell\": \"powershell\" to use bash." |
| 240 | ), |
| 241 | exit_code=-1, |
| 242 | duration_ms=duration_ms, |
| 243 | command=command, |
| 244 | ) |
| 245 | process = await asyncio.create_subprocess_exec( |
| 246 | pwsh_path, |
| 247 | *build_powershell_args(command), |
| 248 | stdin=asyncio.subprocess.PIPE, |
| 249 | stdout=asyncio.subprocess.PIPE, |
| 250 | stderr=asyncio.subprocess.PIPE, |
| 251 | env=_build_hook_env(hook, stdin_data, tool_use_context), |
| 252 | ) |
| 253 | else: |
| 254 | # Default (bash on POSIX via /bin/sh, the historical path). |
| 255 | # An explicit ``shell="bash"`` lands here too — it's a no-op |
| 256 | # alias for ``None`` per the chapter's "defaults to bash" |
| 257 | # contract. |
| 258 | process = await asyncio.create_subprocess_shell( |
| 259 | command, |