{"record":{"id":"e0bed89c08f7c188","repo":"gitbutlerapp/gitbutler","slug":"failed-to-sign-gpg-stdout-stderr","errorCode":null,"errorMessage":"Failed to sign GPG: {stdout} {stderr}","messagePattern":"Failed to sign GPG: (.+?) (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-core/src/commit/mod.rs","lineNumber":435,"sourceCode":"            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {\n                bail!(\n                    \"Could not find '{}'. Please make sure it is in your `PATH` or configure the full path using `gpg.program` in the Git configuration\",\n                    gpg_program.display()\n                )\n            }\n            Err(err) => {\n                return Err(err).context(format!(\"Could not execute GPG program using {cmd:?}\"));\n            }\n        };\n        child.stdin.take().expect(\"configured\").write_all(buffer)?;\n\n        let output = child.wait_with_output()?;\n        if output.status.success() {\n            Ok(BString::new(output.stdout))\n        } else {\n            let stderr = BString::new(output.stderr);\n            let stdout = BString::new(output.stdout);\n            bail!(\"Failed to sign GPG: {stdout} {stderr}\");\n        }\n    }\n}\n\n/// When commits are in conflicting state, they store various trees which to help deal with the conflict.\n///\n/// This also includes variant that represents the blob which contains the\n/// conflicted information.\n#[derive(Debug, Copy, Clone)]\npub enum TreeKind {\n    /// Our tree that caused a conflict during the merge.\n    Ours,\n    /// Their tree that caused a conflict during the merge.\n    Theirs,\n    /// The base of the conflicting mereg.\n    Base,\n    /// The tree that resulted from the merge with auto-resolution enabled.\n    AutoResolution,","sourceCodeStart":417,"sourceCodeEnd":453,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-core/src/commit/mod.rs#L417-L453","documentation":"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.","triggerScenarios":"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.","commonSituations":"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`.","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."],"exampleFix":"# ~/.gitconfig or repo config — before (key missing/rotated)\n[user]\n    signingkey = ABC123OLD\n\n# after\n[user]\n    signingkey = DEF456NEW   # must appear in: gpg --list-secret-keys DEF456NEW","handlingStrategy":"validation","validationCode":"// Rust — validate signing setup before invoking signed-commit APIs\nfn gpg_signing_ready(repo: &gix::Repository) -> bool {\n    let Ok(Some(key)) = repo.config().string(\"user.signingkey\") else {\n        return false; // nothing to validate against\n    };\n    let program = repo\n        .config()\n        .string(\"gpg.program\")\n        .map(|p| p.to_string())\n        .unwrap_or_else(|| \"gpg\".into());\n    std::process::Command::new(program.as_str())\n        .args([\"--list-secret-keys\", &key.to_string()])\n        .stdin(Stdio::null())\n        .stdout(Stdio::null())\n        .stderr(Stdio::null())\n        .status()\n        .map(|s| s.success())\n        .unwrap_or(false)\n}","typeGuard":null,"tryCatchPattern":"// When the sign/commit call fails, surface the embedded GPG stderr to the user\n// (it contains [GNUPG:] status lines) instead of a generic 'commit failed'.\nmatch sign_or_commit() {\n    Ok(v) => v,\n    Err(err) => {\n        let chain = format!(\"{err:#}\");\n        if chain.starts_with(\"Failed to sign GPG\") {\n            return Err(anyhow!(\"Commit signature failed — check `git config user.signingkey` and gpg-agent. Details: {chain}\"));\n        }\n        return Err(err);\n    }\n}","preventionTips":["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."],"tags":["gpg","commit-signing","git-config","subprocess"],"backgroundTag":"gpg-signing-failed","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}