If you’ve ever tried to sign a Git commit with a YubiKey on macOS and hit a cryptic “General error” from GPG — welcome to the scdaemon PC/SC race condition. I hit this problem repeatedly while setting up secure signing workflows for clients, and ended up building gpg-with-retry: a drop-in replacement that makes GPG YubiKey scdaemon macOS errors a thing of the past. The fix is a single Bash script, zero dependencies, MIT license.
- The GPG YubiKey scdaemon macOS Race Condition
- Why Standard Workarounds Fall Short
- How gpg-with-retry Solves It
- Installation in Three Commands
- Configuration and Debug Options
- Real-World Deployment: Zero Failures on M2 Macs
- Technical Notes: What Made This Non-Trivial
- FAQ: GPG YubiKey scdaemon Issues on macOS
- Why does GPG return “General error” when the YubiKey is plugged in?
- Does this work with YubiKey SSH authentication via gpg-agent?
- Does the preflight check slow down every git commit?
- Which YubiKey models and smartcards does this support?
- Get gpg-with-retry
The GPG YubiKey scdaemon macOS Race Condition
On macOS, three processes compete for smartcard access through the PC/SC layer:
- scdaemon (GnuPG) — holds the selected OpenPGP applet
- ctkpcscd (Apple CryptoTokenKit) — periodically polls the card
- ssh-pkcs11-helper (OpenSSH) — accesses the PKCS#11 interface
When ctkpcscd or ssh-pkcs11-helper connects to the YubiKey, it deselects the OpenPGP applet that scdaemon expects to be selected. scdaemon uses its stale connection handle and returns “General error” on the next operation. The YubiKey is fine — scdaemon just lost track of it.
The pain is sharpest during git commit -S. Git treats any GPG error as fatal — no built-in retry, no grace period. You end up running gpgconf --kill scdaemon by hand before every other commit.
Why Standard Workarounds Fall Short
- disable-ccid in scdaemon.conf — reduces frequency but doesn’t eliminate the race
- Manual
pkill scdaemonalias — fragile, interrupts flow - Git hooks — can’t reliably catch GPG exit codes
- Restarting gpg-agent — nuclear option, clears all cached PINs
How gpg-with-retry Solves It
gpg-with-retry acts as a transparent proxy for the gpg binary. Set it as gpg.program in your git config, and it handles the scdaemon lifecycle automatically:
- Preflight check — before the first GPG call, verifies the OpenPGP applet is selected via
SCD SERIALNO - Transparent retry — on a recoverable error, kills scdaemon, flushes PCSC clients, re-establishes the connection, retries
- Smart error classification — “General error” → retry; “Operation cancelled” → exit immediately; “No card” → exit
- Card release — kills scdaemon after each operation to prevent it holding the applet
- Race-free stderr capture — uses a named FIFO instead of process substitution
The net result: git commit -S works on the first try, every time, without any manual intervention.
Installation in Three Commands
cp gpg-with-retry ~/.local/bin/
chmod +x ~/.local/bin/gpg-with-retry
git config --global gpg.program gpg-with-retry Also add this to ~/.gnupg/scdaemon.conf:
disable-ccid
pcsc-shared Configuration and Debug Options
GPG_RETRY_MAX=2— maximum retry attempts (default: 2)GPG_RETRY_DEBUG=1— verbose diagnostic output to stderrGPG_BINARY=/path/to/gpg— override path to the real gpg binary
When something goes wrong, GPG_RETRY_DEBUG=1 git commit -S shows exactly what happened: which error was detected, whether a retry fired, and what state scdaemon was in.
Real-World Deployment: Zero Failures on M2 Macs
I deployed gpg-with-retry for a development team using YubiKey-enforced commit signing across macOS 14 on M2 hardware. Before the fix, the scdaemon race condition caused failed git commit -S roughly one in five attempts. After deploying gpg-with-retry: zero failures.
This kind of practical tooling is part of building secure development infrastructure. I cover the broader picture in my work on IT consulting and risk management, where developer tooling is often the gap between policy and practice.
Technical Notes: What Made This Non-Trivial
- FIFO over process substitution: GPG can exit before process substitution captures all stderr output. A named FIFO blocks until the reader has consumed everything.
- Pattern-based error classification: GPG exit codes are unreliable across versions. The script matches known error strings rather than numeric codes.
- PCSC client cleanup: On some macOS versions, killing scdaemon isn’t enough — ctkpcscd holds a stale handle.
- No external dependencies: Pure POSIX shell. No Python, no Homebrew, no nothing.
FAQ: GPG YubiKey scdaemon Issues on macOS
Why does GPG return “General error” when the YubiKey is plugged in?
This is the PC/SC applet deselection problem. When ctkpcscd or ssh-pkcs11-helper connects to the card, they deselect the OpenPGP applet that scdaemon expects. scdaemon uses its stale connection handle and returns “General error”. The YubiKey is fine — scdaemon just lost track of it. gpg-with-retry detects this pattern and recovers automatically.
Does this work with YubiKey SSH authentication via gpg-agent?
Yes. The wrapper intercepts all calls routed through gpg.program, which covers git commit -S, gpg --sign, gpg --decrypt, and any tool that invokes the gpg binary.
Does the preflight check slow down every git commit?
Only the first call after a scdaemon restart — roughly 100–200ms for the SCD SERIALNO check. Subsequent commits within the same scdaemon session have no overhead.
Which YubiKey models and smartcards does this support?
Any OpenPGP-capable card that works with scdaemon: YubiKey 4, YubiKey 5 (USB-A, USB-C, NFC), Nitrokey, Gnuk, and similar. The wrapper doesn’t communicate with the card directly — it manages scdaemon, which handles all hardware-specific protocol details.
Get gpg-with-retry
gpg-with-retry is open source under the MIT license. If you’re setting up YubiKey-based signing infrastructure for your team, or dealing with GPG reliability issues at scale, reach out — I’d be glad to help.
