feat: add SSH wrapper for Windows OpenSSH bug workaround#13
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| {{- end }} | ||
| {{- if eq .chezmoi.os "windows" }} | ||
| sshCommand = C:/Windows/System32/OpenSSH/ssh.exe | ||
| sshCommand = sh "{{ .chezmoi.homeDir | replace "\\" "/" }}/.ssh/ssh-wrapper.sh" |
There was a problem hiding this comment.
🟡 Path quotes in the SSH command configuration are consumed by Git's config parser, breaking the command for paths with spaces
The file path quotes are stripped by Git's config-file parser (sshCommand = sh "..." at dot_gitconfig.tmpl:35) before the value reaches the shell, so any home directory containing a space causes the SSH command to fail with a word-splitting error.
Impact: Git push/pull/fetch operations silently break on Windows machines where the user profile path contains a space.
Git config quote-stripping mechanism and fix options
Git config uses double quotes as its own quoting syntax (to preserve whitespace and shield comment characters # and ;). When the config file contains:
sshCommand = sh "C:/Users/username/.ssh/ssh-wrapper.sh"
the git config parser sees sh as unquoted text, then " toggles quoted mode, captures C:/Users/username/.ssh/ssh-wrapper.sh as literal text, then " ends quoted mode. The resulting stored value is sh C:/Users/username/.ssh/ssh-wrapper.sh (quotes stripped).
When Git invokes sshCommand, it passes the stored value to sh -c with arguments appended. The shell then word-splits the unquoted path. For a user like C:/Users/John Doe/, the shell sees three tokens: sh, C:/Users/John, Doe/.ssh/ssh-wrapper.sh.
To preserve literal quotes in the value so the shell sees them, the template needs escaped quotes inside a git-config quoted string:
sshCommand = "sh \"{{ .chezmoi.homeDir | replace "\\" "/" }}/.ssh/ssh-wrapper.sh\""
or use the fact that git passes the whole value to sh -c "$val" -- args and restructure accordingly.
Prompt for agents
In dot_gitconfig.tmpl line 35, the double quotes around the ssh-wrapper.sh path are being consumed by Git's own config file parser (Git uses double quotes as config-level quoting delimiters). The stored value ends up without quotes, so the shell word-splits the path when it contains spaces (e.g. a Windows username with a space).
To fix: the literal double quotes must survive git config parsing. Inside a git config value, a literal quote is written as a backslash-quote inside a quoted section. The template line needs to produce something like:
sshCommand = sh \"C:/Users/username/.ssh/ssh-wrapper.sh\"
so that the stored git config value is sh "C:/Users/username/.ssh/ssh-wrapper.sh" and the shell can correctly handle paths with spaces.
Alternatively, since this wrapper is only used on Windows where Git for Windows provides its own sh, you could avoid the quoting issue entirely by using a different invocation strategy, such as putting the entire command in quotes with escaped inner quotes, or by not relying on shell word splitting at all.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if [ $exit_code -ne 0 ]; then | ||
| if grep -q 'close - IO is still pending on closed socket' "$stderr_file"; then | ||
| if grep -q 'Permission denied' "$stderr_file"; then | ||
| cat "$stderr_file" >&2 | ||
| rm -f "$stderr_file" | ||
| exit $exit_code | ||
| fi | ||
| grep -v 'close - IO is still pending on closed socket' "$stderr_file" >&2 | ||
| rm -f "$stderr_file" | ||
| exit 0 | ||
| fi |
There was a problem hiding this comment.
🚩 Wrapper script suppresses all real SSH errors that happen to co-occur with the IO pending message
The wrapper script at private_dot_ssh/ssh-wrapper.sh:13-23 only checks for Permission denied as a signal of a "real" failure when the IO pending bug message is detected. Other genuine SSH errors (e.g., Host key verification failed, Connection refused, No route to host) that happen to co-occur with the IO pending stderr message would be suppressed and the script would return exit 0. This means certain real SSH failures could be silently swallowed. Whether this matters in practice depends on whether the Windows OpenSSH bug ever produces the IO pending message alongside other real errors.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Adds
~/.ssh/ssh-wrapper.shto work around a Windows OpenSSH 9.5 bug where non-interactive SSH crashes withclose - IO is still pending on closed socketafter successful operations, causing Git to interpret the non-zero exit code as a failure.The wrapper captures stderr, detects the bug message, and returns exit 0 — unless
Permission deniedis also present (real auth failure).dot_gitconfig.tmplnow references the wrapper with a dynamic home path:instead of the previous direct
C:/Windows/System32/OpenSSH/ssh.exe..chezmoiignoreexcludesssh-wrapper.shon non-Windows (bug is Windows-specific).Link to Devin session: https://app.devin.ai/sessions/b78d2510243149569051fae15809fef4
Requested by: @caipira113