* 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>,
)
| 828 | * See docs/design/ps-shell-selection.md §5.1. |
| 829 | */ |
| 830 | async function execCommandHook( |
| 831 | hook: HookCommand & { type: 'command' }, |
| 832 | hookEvent: HookEvent | 'StatusLine' | 'FileSuggestion', |
| 833 | hookName: string, |
| 834 | jsonInput: string, |
| 835 | signal: AbortSignal, |
| 836 | hookId: string, |
| 837 | hookIndex?: number, |
| 838 | pluginRoot?: string, |
| 839 | pluginId?: string, |
| 840 | skillRoot?: string, |
| 841 | forceSyncExecution?: boolean, |
| 842 | requestPrompt?: (request: PromptRequest) => Promise<PromptResponse>, |
| 843 | ): Promise<{ |
| 844 | stdout: string |
| 845 | stderr: string |
| 846 | output: string |
| 847 | status: number |
| 848 | aborted?: boolean |
| 849 | backgrounded?: boolean |
| 850 | }> { |
| 851 | // Gated to once-per-session events to keep diag_log volume bounded. |
| 852 | // started/completed live inside the try/finally so setup-path throws |
| 853 | // don't orphan a started marker — that'd be indistinguishable from a hang. |
| 854 | const shouldEmitDiag = |
| 855 | hookEvent === 'SessionStart' || |
| 856 | hookEvent === 'Setup' || |
| 857 | hookEvent === 'SessionEnd' |
| 858 | const diagStartMs = Date.now() |
| 859 | let diagExitCode: number | undefined |
| 860 | let diagAborted = false |
| 861 | |
| 862 | const isWindows = getPlatform() === 'windows' |
| 863 | |
| 864 | // -- |
| 865 | // Per-hook shell selection (phase 1 of docs/design/ps-shell-selection.md). |
| 866 | // Resolution order: hook.shell → DEFAULT_HOOK_SHELL. The defaultShell |
| 867 | // fallback (settings.defaultShell) is phase 2 — not wired yet. |
| 868 | // |
| 869 | // The bash path is the historical default and stays unchanged. The |
| 870 | // PowerShell path deliberately skips the Windows-specific bash |
| 871 | // accommodations (cygpath conversion, .sh auto-prepend, POSIX-quoted |
| 872 | // SHELL_PREFIX). |
| 873 | const shellType = hook.shell ?? DEFAULT_HOOK_SHELL |
| 874 | |
| 875 | const isPowerShell = shellType === 'powershell' |
| 876 | |
| 877 | // -- |
| 878 | // Windows bash path: hooks run via Git Bash (Cygwin), NOT cmd.exe. |
| 879 | // |
| 880 | // This means every path we put into env vars or substitute into the command |
| 881 | // string MUST be a POSIX path (/c/Users/foo), not a Windows path |
| 882 | // (C:\Users\foo or C:/Users/foo). Git Bash cannot resolve Windows paths. |
| 883 | // |
| 884 | // windowsPathToPosixPath() is pure-JS regex conversion (no cygpath shell-out): |
| 885 | // C:\Users\foo -> /c/Users/foo, UNC preserved, slashes flipped. Memoized |
| 886 | // (LRU-500) so repeated calls are cheap. |
| 887 | // |
no test coverage detected