git commit -S fails under Claude Code on WSL: route pinentry to Gpg4win
Signing commits from inside Claude Code kept failing on my Ubuntu-on-WSL setup:
error: gpg failed to sign the data
error: unable to sign the tag
gpg: signing failed: No pinentry
Running the same git commit -S by hand in my terminal worked. That gap is the whole story.
Why it happens
Claude Code executes shell commands in a non-interactive shell with no controlling TTY — the terminal is owned by the session UI, not by the command. GPG_TTY evaluates to not a tty there.
A terminal-based pinentry (pinentry-curses, pinentry-tty) has to draw its passphrase prompt onto a TTY. With none available, gpg either hangs forever or gives up with No pinentry. This isn’t specific to gpg: sudo prompts, ssh key passphrases, and git rebase -i fail the same way for the same reason.
It’s intermittent, which makes it confusing. gpg-agent caches your passphrase for default-cache-ttl seconds (600 by default), so signing succeeds while the cache is warm and fails the moment a prompt becomes necessary. I had a commit sign fine at 00:25 and a tag refuse at 00:37.
The fix: prompt outside the terminal
Install Gpg4win on the Windows side and point WSL’s gpg-agent at its pinentry. A Windows GUI dialog renders on the desktop — out-of-band, needing no TTY at all — so signing works from a non-interactive shell.
~/.gnupg/gpg-agent.conf:
pinentry-program /mnt/c/Program Files/Gpg4win/bin/pinentry.exe
Then restart the agent:
gpgconf --kill gpg-agent
Use --kill, not --reload — a changed pinentry-program isn’t picked up on reload. The agent restarts on the next gpg call.
Verify:
echo test | gpg --clearsign -u <YOUR_KEY_ID> > /dev/null
A Windows passphrase dialog should pop up. After that, git commit -S and git tag -s work from inside Claude Code.
Gotchas
- Spaces in the path need no quoting or escaping. gpg-agent takes the entire rest of the line as the filename.
- Check the path still exists after a Gpg4win upgrade. Mine broke when Gpg4win moved into the 64-bit tree: my config still pointed at
C:\Program Files (x86)\GnuPG\bin\pinentry.exe, which had been emptied out, while the working binary was atC:\Program Files\Gpg4win\bin\pinentry.exe. pinentry-w32.exesits in the same directory as a fallback if the Qt build misbehaves across the WSL boundary.-
Diagnose without hanging:
echo test | gpg --batch --pinentry-mode error --clearsign -u <KEY> # fails fast gpg-connect-agent 'keyinfo --list' /bye # is the key cached? -
Raise the cache if a commit-then-tag sequence re-prompts:
default-cache-ttl 3600 max-cache-ttl 7200 export GPG_TTY=$(tty)in your shell rc matters only for terminal pinentry. Harmless to keep, irrelevant to this route.
Alternatives I passed on
Unlock in a real terminal, then let the agent work within the cache window. Fragile — it expires mid-session, which is exactly what bit me.
SSH-format signing (git config gpg.format ssh with an ssh-agent-held key) sidesteps pinentry entirely, but changes how verification appears on GitHub.
The GUI route is the only one that’s unconditionally safe from a non-interactive shell.