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
- 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.
- Verify the configured key exists: `git config user.signingkey` then `gpg --list-secret-keys <that-id>`; import or regenerate if absent.
- Test the exact invocation by hand: `echo test | gpg --status-fd=2 -bsau $(git config user.signingkey) -` and fix whatever it reports.
- Ensure the agent can prompt: `export GPG_TTY=$(tty)` and `gpgconf --launch gpg-agent`, or configure a working pinentry in `~/.gnupg/gpg-agent.conf`.
- 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
- In CI or headless environments run `gpg --batch --yes --import` of the secret key and use an agent with cache-only pinentry before any signed commit API.
- Pin `gpg.program` to an absolute path so PATH differences cannot swap the binary.
- Add a startup check that `gpg --list-secret-keys <signingkey>` succeeds before enabling signing features.
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
- Could not find '{}'. Please make sure it is in your `PATH` o
- When using GitButler's API to summarize code, you must be lo
- Invalid token format
- failed to discover GitButler branch with 'but --json status'
- Failed to sign SSH: {stdout} {stderr}
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/e0bed89c08f7c188.
Report an issue: GitHub.