Skip to content

fix: use forward slashes in hook paths for bash compatibility#42

Open
sleepyy-dog wants to merge 1 commit intoshanselman:mainfrom
sleepyy-dog:fix/bash-path-backslash
Open

fix: use forward slashes in hook paths for bash compatibility#42
sleepyy-dog wants to merge 1 commit intoshanselman:mainfrom
sleepyy-dog:fix/bash-path-backslash

Conversation

@sleepyy-dog
Copy link
Copy Markdown

Problem

When toasty --install claude (or gemini) runs on Windows, GetModuleFileNameW() returns a backslash path like D:\app\toasty\toasty.exe. This path is written into the hook command in settings.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.exe gets mangled to D:apptoastytoasty.exe, producing:

Stop hook error: Failed with non-blocking status code:
/usr/bin/bash: line 1: D:apptoastytoasty.exe: command not found

Additionally, escape_json_string() manually escapes \\ for JSON, but WinRT's JsonValue::CreateStringValue() + Stringify() already handles JSON serialization, causing double-escaping of backslashes.

Fix

  • Add normalize_path_for_shell() that converts \ to /
  • Apply it in install_claude(), install_gemini(), and their dry-run output
  • Remove the now-unnecessary escape_json_string() calls for these functions

D:/app/toasty/toasty.exe is accepted by Windows APIs and safe in all shell contexts (bash, cmd, PowerShell).

Note

install_copilot() already avoids this issue by using bare toasty (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:

  • Before: toasty --install claude → hook fails with D:apptoastytoasty.exe: command not found
  • After: manually changing path to D:/app/toasty/toasty.exe → hook executes successfully, toast notification appears

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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 exePath contains spaces, bash will treat it as multiple tokens and the hook will fail. Quote shellPath when 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 shellPath in 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.

Comment thread main.cpp
Comment on lines +851 to +854
std::wstring normalize_path_for_shell(const std::wstring& path) {
std::wstring result = path;
std::replace(result.begin(), result.end(), L'\\', L'/');
return result;
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread main.cpp
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\"";
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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\"";

Copilot uses AI. Check for mistakes.
Comment thread main.cpp
Comment on lines +1530 to 1532
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";
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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";

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants