Skip to content

Commit bdec7fc

Browse files
committed
fix: use forward slashes in hook paths for bash compatibility
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.
1 parent 75b3ac9 commit bdec7fc

File tree

1 file changed

+19
-8
lines changed

1 file changed

+19
-8
lines changed

main.cpp

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,17 @@ std::wstring get_exe_path() {
843843
return std::wstring(exePath);
844844
}
845845

846+
// Convert backslashes to forward slashes for cross-shell compatibility.
847+
// Claude Code and Gemini CLI execute hook commands via bash (Git Bash / MSYS2
848+
// on Windows). Unquoted backslashes in bash are escape characters, so a path
849+
// like D:\app\toasty\toasty.exe is mangled to D:apptoastytoasty.exe.
850+
// Forward slashes are accepted by Windows APIs and safe in bash.
851+
std::wstring normalize_path_for_shell(const std::wstring& path) {
852+
std::wstring result = path;
853+
std::replace(result.begin(), result.end(), L'\\', L'/');
854+
return result;
855+
}
856+
846857
// Expand environment variables in a path
847858
std::wstring expand_env(const std::wstring& path) {
848859
wchar_t expanded[MAX_PATH];
@@ -994,8 +1005,8 @@ bool install_claude(const std::wstring& exePath) {
9941005
JsonObject innerHook;
9951006
innerHook.SetNamedValue(L"type", JsonValue::CreateStringValue(L"command"));
9961007

997-
std::wstring escapedPath = escape_json_string(exePath);
998-
std::wstring command = escapedPath + L" \"Task complete\" -t \"Claude Code\"";
1008+
std::wstring shellPath = normalize_path_for_shell(exePath);
1009+
std::wstring command = shellPath + L" \"Task complete\" -t \"Claude Code\"";
9991010
innerHook.SetNamedValue(L"command", JsonValue::CreateStringValue(command));
10001011
innerHook.SetNamedValue(L"timeout", JsonValue::CreateNumberValue(5000));
10011012

@@ -1052,8 +1063,8 @@ bool install_gemini(const std::wstring& exePath) {
10521063
innerHook.SetNamedValue(L"type", JsonValue::CreateStringValue(L"command"));
10531064
innerHook.SetNamedValue(L"name", JsonValue::CreateStringValue(L"toasty-notification"));
10541065

1055-
std::wstring escapedPath = escape_json_string(exePath);
1056-
std::wstring command = escapedPath + L" \"Gemini finished\" -t \"Gemini\"";
1066+
std::wstring shellPath = normalize_path_for_shell(exePath);
1067+
std::wstring command = shellPath + L" \"Gemini finished\" -t \"Gemini\"";
10571068
innerHook.SetNamedValue(L"command", JsonValue::CreateStringValue(command));
10581069

10591070
JsonArray innerHooks;
@@ -1516,15 +1527,15 @@ void handle_install(const std::wstring& agent) {
15161527
if (installClaude) {
15171528
std::wstring configPath = expand_env(L"%USERPROFILE%\\.claude\\settings.json");
15181529
std::wcout << L"[dry-run] Would write: " << configPath << L"\n";
1519-
std::wstring escapedPath = escape_json_string(exePath);
1520-
std::wcout << L"[dry-run] Hook command: " << escapedPath << L" \"Task complete\" -t \"Claude Code\"\n";
1530+
std::wstring shellPath = normalize_path_for_shell(exePath);
1531+
std::wcout << L"[dry-run] Hook command: " << shellPath << L" \"Task complete\" -t \"Claude Code\"\n";
15211532
std::wcout << L"[dry-run] Hook type: Stop\n";
15221533
}
15231534
if (installGemini) {
15241535
std::wstring configPath = expand_env(L"%USERPROFILE%\\.gemini\\settings.json");
15251536
std::wcout << L"[dry-run] Would write: " << configPath << L"\n";
1526-
std::wstring escapedPath = escape_json_string(exePath);
1527-
std::wcout << L"[dry-run] Hook command: " << escapedPath << L" \"Gemini finished\" -t \"Gemini\"\n";
1537+
std::wstring shellPath = normalize_path_for_shell(exePath);
1538+
std::wcout << L"[dry-run] Hook command: " << shellPath << L" \"Gemini finished\" -t \"Gemini\"\n";
15281539
std::wcout << L"[dry-run] Hook type: AfterAgent\n";
15291540
}
15301541
if (installCopilot) {

0 commit comments

Comments
 (0)