* Execute a command-based hook using bash or PowerShell. * * Shell resolution: hook.shell → 'bash'. PowerShell hooks spawn pwsh * with -NoProfile -NonInteractive -Command and skip bash-specific prep * (POSIX path conversion, .sh auto-prepend, CLAUDE_CODE_SHELL_PREFIX). * See docs/design/ps-shel
(
hook: HookCommand & { type: 'command' },
hookEvent: HookEvent | 'StatusLine' | 'FileSuggestion',
hookName: string,
jsonInput: string,
signal: AbortSignal,
hookId: string,
hookIndex?: number,
pluginRoot?: string,
pluginId?: string,
skillRoot?: string,
forceSyncExecution?: boolean,
requestPrompt?: (request: PromptRequest) => Promise<PromptResponse>,
)
| 745 | * See docs/design/ps-shell-selection.md §5.1. |
| 746 | */ |
| 747 | async function execCommandHook( |
| 748 | hook: HookCommand & { type: 'command' }, |
| 749 | hookEvent: HookEvent | 'StatusLine' | 'FileSuggestion', |
| 750 | hookName: string, |
| 751 | jsonInput: string, |
| 752 | signal: AbortSignal, |
| 753 | hookId: string, |
| 754 | hookIndex?: number, |
| 755 | pluginRoot?: string, |
| 756 | pluginId?: string, |
| 757 | skillRoot?: string, |
| 758 | forceSyncExecution?: boolean, |
| 759 | requestPrompt?: (request: PromptRequest) => Promise<PromptResponse>, |
| 760 | ): Promise<{ |
| 761 | stdout: string |
| 762 | stderr: string |
| 763 | output: string |
| 764 | status: number |
| 765 | aborted?: boolean |
| 766 | backgrounded?: boolean |
| 767 | }> { |
| 768 | // Gated to once-per-session events to keep diag_log volume bounded. |
| 769 | // started/completed live inside the try/finally so setup-path throws |
| 770 | // don't orphan a started marker — that'd be indistinguishable from a hang. |
| 771 | const shouldEmitDiag = |
| 772 | hookEvent === 'SessionStart' || |
| 773 | hookEvent === 'Setup' || |
| 774 | hookEvent === 'SessionEnd' |
| 775 | const diagStartMs = Date.now() |
| 776 | let diagExitCode: number | undefined |
| 777 | let diagAborted = false |
| 778 | |
| 779 | const isWindows = getPlatform() === 'windows' |
| 780 | |
| 781 | // -- |
| 782 | // Per-hook shell selection (phase 1 of docs/design/ps-shell-selection.md). |
| 783 | // Resolution order: hook.shell → DEFAULT_HOOK_SHELL. The defaultShell |
| 784 | // fallback (settings.defaultShell) is phase 2 — not wired yet. |
| 785 | // |
| 786 | // The bash path is the historical default and stays unchanged. The |
| 787 | // PowerShell path deliberately skips the Windows-specific bash |
| 788 | // accommodations (cygpath conversion, .sh auto-prepend, POSIX-quoted |
| 789 | // SHELL_PREFIX). |
| 790 | const shellType = hook.shell ?? DEFAULT_HOOK_SHELL |
| 791 | |
| 792 | const isPowerShell = shellType === 'powershell' |
| 793 | |
| 794 | // -- |
| 795 | // Windows bash path: hooks run via Git Bash (Cygwin), NOT cmd.exe. |
| 796 | // |
| 797 | // This means every path we put into env vars or substitute into the command |
| 798 | // string MUST be a POSIX path (/c/Users/foo), not a Windows path |
| 799 | // (C:\Users\foo or C:/Users/foo). Git Bash cannot resolve Windows paths. |
| 800 | // |
| 801 | // windowsPathToPosixPath() is pure-JS regex conversion (no cygpath shell-out): |
| 802 | // C:\Users\foo -> /c/Users/foo, UNC preserved, slashes flipped. Memoized |
| 803 | // (LRU-500) so repeated calls are cheap. |
| 804 | // |
no test coverage detected