From 8e1f9eabc01f67e2c774e34f22e0c7913fc56cd2 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 20 Jul 2026 18:14:26 +0200 Subject: [PATCH] git: Only prompt for GPG passphrase when gpg-agent cannot supply it (#61265) Closes #61141 Follow-up https://github.com/zed-industries/zed/pull/58791 # Objective Currently after https://github.com/zed-industries/zed/pull/58791 landed, we now always ask for the gpg passphrase on every commit which is not ideal, since some people have a bigger **ttl** configured so they don't have/want to re-enter their passphrase everytime on every commit. So we should cache the signature so the user does not have to re-enter their passphrase everytime, which also fixes the case for unprotected keys which obviously don't have a passphrase. So we pre-check if we can sign the commit if so we should not ask for the passphrase at all. ## Solution The solution was also suggested inside this https://github.com/zed-industries/zed/issues/61141 issue, to first check if the signature was cached, if that is is the case use that to sign the commit. If that fails we should fallback to the loopback mode and Zed should prompt for your passphrase once and your signature should be cached again. ## Testing **The basic setup for gpg signing**: 1. Create a key `gpg --full-generate-key` 2. Run `git config --global user.signingkey ` 3. Run `git config --global commit.gpgsign true` 4. Run & copy result from `gpg --armor --export ` and submit your public key to github https://github.com/settings/gpg/new **Testing the new flow were the signature is cached**: 5. Run `gpgconf --reload gpg-agent` to reset cached signature 6. Make a change and try committing 7. See that it promts for your passphrase 8. enter passphrase 9. Make a change and try committing 10. Notice it does not re-promts for your passphrase ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [ ] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [ ] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable ## Showcase **Before** (**Note** before you were prompted everytime when you commit) https://github.com/user-attachments/assets/0a967bb1-5eff-4224-9e9a-995e411f8d3a **After** (**Note** you are only prompted once and using the keychain after instead) https://github.com/user-attachments/assets/5eee6afb-5972-44d8-afdd-6416a189db32 --- **Note**: I used AI as an assistance to write this PR. Release Notes: - Git: Fixed the GPG signing passphrase prompt appearing on every commit even when gpg-agent had the passphrase cached or the key was unprotected. --- crates/askpass/src/askpass.rs | 42 +++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/crates/askpass/src/askpass.rs b/crates/askpass/src/askpass.rs index af5d0d31ad..8092e36f3f 100644 --- a/crates/askpass/src/askpass.rs +++ b/crates/askpass/src/askpass.rs @@ -504,11 +504,17 @@ fn generate_gpg_wrapper_script( .try_quote_prefix_aware("Enter passphrase for your Git signing key:") .context("Failed to shell-escape gpg passphrase prompt")?; - // The wrapper inspects gpg's arguments and only prompts for a passphrase when - // git asks it to *sign* (e.g. `gpg -bsau `). Other invocations such as - // signature verification (`--verify`) run gpg unchanged so we never pop a - // spurious modal. When signing, we feed the passphrase to gpg on fd 3 via - // `--passphrase-fd 3` in loopback mode, so no pinentry/terminal is required. + // The wrapper only intervenes when git asks gpg to *sign* (e.g. `gpg -bsau + // `); other invocations like `--verify` run unchanged. For signing we + // first try plain gpg so gpg-agent/keychain can supply a cached or empty + // passphrase silently, and only fall back to asking Zed (loopback mode, fd + // 3) when that fails, e.g. the "Inappropriate ioctl for device" case with no + // TTY for pinentry. + // + // git streams the payload on stdin (readable once) and reads the signature + // from stdout, so we buffer stdin to replay it into both attempts and buffer + // the first attempt's output, forwarding it only if it succeeds. The + // passphrase goes to fd 3 via a pipe. Ok(format!( r#"#!/bin/sh for arg in "$@"; do @@ -527,11 +533,29 @@ if [ -z "${{is_signing}}" ]; then exec {gpg_program} "$@" fi -# Signing: ask Zed for the passphrase, then hand it to gpg on fd 3. +# Signing. Buffer stdin (the payload) and the first attempt's output +# so we can retry cleanly on failure without git seeing partial output. +tmpdir=$(mktemp -d) || exit 1 +trap 'rm -rf "$tmpdir"' EXIT +payload="$tmpdir/payload" +signature="$tmpdir/signature" +status="$tmpdir/status" +cat > "$payload" || exit 1 + +# First try letting gpg-agent/keychain supply the passphrase without any +# interactive pinentry. If that succeeds (cached passphrase) +# forward its output and we're done, so Zed never shows a modal. +if {gpg_program} --pinentry-mode error "$@" < "$payload" > "$signature" 2> "$status"; then + cat "$status" >&2 + cat "$signature" + exit 0 +fi + +# The silent attempt failed: ask Zed for the passphrase, then hand it to gpg on +# fd 3 using loopback mode so no pinentry/terminal is required. passphrase=$(printf '%s\0' {prompt} | {askpass_program} --askpass={askpass_socket} 2>/dev/null) -exec {gpg_program} --pinentry-mode loopback --passphrase-fd 3 "$@" 3<