fix: use forward slashes in hook paths for bash compatibility#42
fix: use forward slashes in hook paths for bash compatibility#42sleepyy-dog wants to merge 1 commit intoshanselman:mainfrom
Conversation
On Windows, Claude Code and Gemini CLI execute hook commands via bash
(Git Bash / MSYS2). GetModuleFileNameW returns backslash paths like
D:\app\toasty\toasty.exe, but unquoted backslashes in bash are escape
characters — \a becomes a (bell), \t becomes tab — mangling the path
to D:apptoastytoasty.exe ("command not found").
Additionally, escape_json_string() manually escapes backslashes for
JSON, but WinRT's JsonValue::CreateStringValue + Stringify already
handles JSON serialization, causing double-escaping.
Fix: normalize exe path to forward slashes (D:/app/toasty/toasty.exe)
before building the hook command. Forward slashes are accepted by
Windows APIs and safe in all shell contexts.
Affects: install_claude(), install_gemini(), and their dry-run output.
There was a problem hiding this comment.
Pull request overview
This PR fixes Windows hook installation for Claude Code and Gemini by ensuring the generated hook command uses forward slashes (avoiding bash backslash-escape mangling) and by removing redundant manual JSON escaping when using WinRT JSON serialization.
Changes:
- Added
normalize_path_for_shell()to convert\to/for bash-safe execution on Windows. - Updated Claude/Gemini hook installation to use the normalized path (including dry-run output).
- Removed
escape_json_string()usage for Claude/Gemini hook command generation.
Comments suppressed due to low confidence (2)
main.cpp:1068
- The hook command concatenates the executable path without quoting it. If
exePathcontains spaces, bash will treat it as multiple tokens and the hook will fail. QuoteshellPathwhen constructing the command string.
std::wstring shellPath = normalize_path_for_shell(exePath);
std::wstring command = shellPath + L" \"Gemini finished\" -t \"Gemini\"";
innerHook.SetNamedValue(L"command", JsonValue::CreateStringValue(command));
main.cpp:1539
- Dry-run output prints the hook command with an unquoted executable path. If the path contains spaces, bash will split it and the hook won't work. Quote
shellPathin the printed command so the dry-run matches the required hook format.
std::wstring shellPath = normalize_path_for_shell(exePath);
std::wcout << L"[dry-run] Hook command: " << shellPath << L" \"Gemini finished\" -t \"Gemini\"\n";
std::wcout << L"[dry-run] Hook type: AfterAgent\n";
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| std::wstring normalize_path_for_shell(const std::wstring& path) { | ||
| std::wstring result = path; | ||
| std::replace(result.begin(), result.end(), L'\\', L'/'); | ||
| return result; |
There was a problem hiding this comment.
normalize_path_for_shell uses std::replace, but this file doesn't include <algorithm>. This can break compilation on toolchains that don't provide it via transitive includes; please add #include <algorithm> explicitly.
| std::wstring escapedPath = escape_json_string(exePath); | ||
| std::wstring command = escapedPath + L" \"Task complete\" -t \"Claude Code\""; | ||
| std::wstring shellPath = normalize_path_for_shell(exePath); | ||
| std::wstring command = shellPath + L" \"Task complete\" -t \"Claude Code\""; |
There was a problem hiding this comment.
The hook command concatenates the executable path without quoting it. If exePath contains spaces (e.g., under Program Files), bash will split the command and fail. Wrap the normalized path in quotes when building command (and keep the args outside or properly quoted).
This issue also appears on line 1066 of the same file.
| std::wstring command = shellPath + L" \"Task complete\" -t \"Claude Code\""; | |
| // Quote the executable path so spaces in the path don't break the command | |
| std::wstring command = L"\"" + shellPath + L"\" \"Task complete\" -t \"Claude Code\""; |
| std::wstring shellPath = normalize_path_for_shell(exePath); | ||
| std::wcout << L"[dry-run] Hook command: " << shellPath << L" \"Task complete\" -t \"Claude Code\"\n"; | ||
| std::wcout << L"[dry-run] Hook type: Stop\n"; |
There was a problem hiding this comment.
Dry-run output prints the hook command with an unquoted executable path. If the path contains spaces, users copying this command into a bash hook will still hit a failure. Print the path quoted to match the actual safe hook command format.
This issue also appears on line 1537 of the same file.
See below for a potential fix:
std::wcout << L"[dry-run] Hook command: \"" << shellPath << L"\" \"Task complete\" -t \"Claude Code\"\n";
std::wcout << L"[dry-run] Hook type: Stop\n";
}
if (installGemini) {
std::wstring configPath = expand_env(L"%USERPROFILE%\\.gemini\\settings.json");
std::wcout << L"[dry-run] Would write: " << configPath << L"\n";
std::wstring shellPath = normalize_path_for_shell(exePath);
std::wcout << L"[dry-run] Hook command: \"" << shellPath << L"\" \"Gemini finished\" -t \"Gemini\"\n";
Problem
When
toasty --install claude(orgemini) runs on Windows,GetModuleFileNameW()returns a backslash path likeD:\app\toasty\toasty.exe. This path is written into the hook command insettings.json.Claude Code and Gemini CLI execute hook commands via bash (Git Bash / MSYS2 on Windows). In bash, unquoted backslashes are escape characters:
\a→ bell (0x07)\t→ tab (0x09)So
D:\app\toasty\toasty.exegets mangled toD:apptoastytoasty.exe, producing:Additionally,
escape_json_string()manually escapes\→\for JSON, but WinRT'sJsonValue::CreateStringValue()+Stringify()already handles JSON serialization, causing double-escaping of backslashes.Fix
normalize_path_for_shell()that converts\to/install_claude(),install_gemini(), and their dry-run outputescape_json_string()calls for these functionsD:/app/toasty/toasty.exeis accepted by Windows APIs and safe in all shell contexts (bash, cmd, PowerShell).Note
install_copilot()already avoids this issue by using baretoasty(assuming PATH) for its bash hook — this PR applies the same shell-safety principle to Claude and Gemini installs which use full paths.Testing
Verified on Windows 11 with Git Bash (MSYS2) + Claude Code:
toasty --install claude→ hook fails withD:apptoastytoasty.exe: command not foundD:/app/toasty/toasty.exe→ hook executes successfully, toast notification appears