gitbutlerapp/gitbutler · error · anyhow::Error

Failed to sign GPG: {stdout} {stderr}

Error message

Failed to sign GPG: {stdout} {stderr}

What it means

Thrown by but_core's commit signing helper when the external GPG program (from `gpg.program`, defaulting to `gpg`) is spawned with `--status-fd=2 -bsau <key> -`, fed the buffer on stdin, and exits with a non-zero status. The message embeds the child's raw stdout and stderr, which for GPG includes the `--status-fd` lines (e.g. `[GNUPG:] INV_SGNR`, `NO_SECKEY`, `BAD_PASSPHRASE`). It means the program ran but refused or failed to produce a signature.

Source

Thrown at crates/but-core/src/commit/mod.rs:435

            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
                bail!(
                    "Could not find '{}'. Please make sure it is in your `PATH` or configure the full path using `gpg.program` in the Git configuration",
                    gpg_program.display()
                )
            }
            Err(err) => {
                return Err(err).context(format!("Could not execute GPG program using {cmd:?}"));
            }
        };
        child.stdin.take().expect("configured").write_all(buffer)?;

        let output = child.wait_with_output()?;
        if output.status.success() {
            Ok(BString::new(output.stdout))
        } else {
            let stderr = BString::new(output.stderr);
            let stdout = BString::new(output.stdout);
            bail!("Failed to sign GPG: {stdout} {stderr}");
        }
    }
}

/// When commits are in conflicting state, they store various trees which to help deal with the conflict.
///
/// This also includes variant that represents the blob which contains the
/// conflicted information.
#[derive(Debug, Copy, Clone)]
pub enum TreeKind {
    /// Our tree that caused a conflict during the merge.
    Ours,
    /// Their tree that caused a conflict during the merge.
    Theirs,
    /// The base of the conflicting mereg.
    Base,
    /// The tree that resulted from the merge with auto-resolution enabled.
    AutoResolution,

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Read the embedded stderr in the message: `[GNUPG:] NO_SECKEY` means the secret key is missing, `BAD_PASSPHRASE` means agent/pinentry trouble, `INV_SGNR` means the signingkey id is wrong.
  2. Verify the configured key exists: `git config user.signingkey` then `gpg --list-secret-keys <that-id>`; import or regenerate if absent.
  3. Test the exact invocation by hand: `echo test | gpg --status-fd=2 -bsau $(git config user.signingkey) -` and fix whatever it reports.
  4. Ensure the agent can prompt: `export GPG_TTY=$(tty)` and `gpgconf --launch gpg-agent`, or configure a working pinentry in `~/.gnupg/gpg-agent.conf`.
  5. If `gpg.program` is customized, point it at the absolute path of a real GPG binary or remove the override.

Example fix

# ~/.gitconfig or repo config — before (key missing/rotated)
[user]
    signingkey = ABC123OLD

# after
[user]
    signingkey = DEF456NEW   # must appear in: gpg --list-secret-keys DEF456NEW
Defensive patterns

Strategy: validation

Validate before calling

// Rust — validate signing setup before invoking signed-commit APIs
fn gpg_signing_ready(repo: &gix::Repository) -> bool {
    let Ok(Some(key)) = repo.config().string("user.signingkey") else {
        return false; // nothing to validate against
    };
    let program = repo
        .config()
        .string("gpg.program")
        .map(|p| p.to_string())
        .unwrap_or_else(|| "gpg".into());
    std::process::Command::new(program.as_str())
        .args(["--list-secret-keys", &key.to_string()])
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

Try / catch

// When the sign/commit call fails, surface the embedded GPG stderr to the user
// (it contains [GNUPG:] status lines) instead of a generic 'commit failed'.
match sign_or_commit() {
    Ok(v) => v,
    Err(err) => {
        let chain = format!("{err:#}");
        if chain.starts_with("Failed to sign GPG") {
            return Err(anyhow!("Commit signature failed — check `git config user.signingkey` and gpg-agent. Details: {chain}"));
        }
        return Err(err);
    }
}

Prevention

When it happens

Trigger: Calling commit/signing APIs with `commit.gpgsign=true` (or requesting a signed commit) when `user.signingkey` points to a key that is expired, deleted, not present in the local secret-keyring, or when pinentry cannot ask for the passphrase (no tty/agent in GUI or daemon contexts). Also triggered by a `gpg.program` override that is a wrapper script exiting non-zero.

Common situations: Key generated on another machine and never imported; `user.signingkey` still referencing an old key id after rotation; headless/desktop environment where `gpg-agent` cannot spawn pinentry (GPG_TTY unset, `pinentry-mac` vs `pinentry` mismatch); smartcard/YubiKey unplugged; gpgsm vs gpg confusion in `gpg.program`.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/e0bed89c08f7c188. Report an issue: GitHub.