{"record":{"id":"4a37683361bf4cf0","repo":"gitbutlerapp/gitbutler","slug":"failed-to-sign-ssh-stdout-stderr","errorCode":null,"errorMessage":"Failed to sign SSH: {stdout} {stderr}","messagePattern":"Failed to sign SSH: (.+?) (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-core/src/commit/mod.rs","lineNumber":397,"sourceCode":"                .with_context(|| format!(\"Didn't trust 'user.signingKey': {signing_key}\"))?;\n            signing_cmd\n                .arg(signing_key)\n                .arg(buffer_file_to_sign_path.to_path_buf())\n        };\n        let output = into_command(signing_cmd)\n            .stderr(Stdio::piped())\n            .stdout(Stdio::piped())\n            .stdin(Stdio::null())\n            .output()?;\n\n        if output.status.success() {\n            let signature_path = buffer_file_to_sign_path.with_extension(\"sig\");\n            let sig_data = std::fs::read(signature_path)?;\n            Ok(BString::new(sig_data))\n        } else {\n            let stderr = BString::new(output.stderr);\n            let stdout = BString::new(output.stdout);\n            bail!(\"Failed to sign SSH: {stdout} {stderr}\");\n        }\n    } else {\n        let gpg_program = match config.trusted_path(\"gpg.program\")? {\n            Some(program) if !program.as_os_str().is_empty() => program,\n            _ => Path::new(\"gpg\").into(),\n        };\n\n        let mut cmd = into_command(\n            prepare_with_shell_on_windows(&gpg_program)\n                .args([\"--status-fd=2\", \"-bsau\"])\n                .arg(gix::path::from_bstring(signing_key))\n                .arg(\"-\"),\n        );\n        cmd.stdout(Stdio::piped())\n            .stderr(Stdio::piped())\n            .stdin(Stdio::piped());\n\n        let mut child = match cmd.spawn() {","sourceCodeStart":379,"sourceCodeEnd":415,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-core/src/commit/mod.rs#L379-L415","documentation":"The SSH signing path runs ssh-keygen -Y sign -n git -f <key> [-U] over a temp file; when the process exits non-zero, this bail surfaces both its stdout and stderr, which contain ssh-keygen's actual complaint (agent issues, unreadable key, unsupported flags). The .sig sidecar file is only read on success.","triggerScenarios":"Committing with gpg.format=ssh when the private key is not loaded in ssh-agent (the -U flag requires the key in the agent), user.signingkey points to a missing/unreadable key file, the key is passphrase-protected without an agent, or an ssh-keygen too old for -Y signing (< OpenSSH 8.0) or missing gpg.ssh.program support.","commonSituations":"headless CI without SSH_AUTH_SOCK; machines where the agent starts after the app; gpg.ssh.program set to a wrapper that fails; literal key vs file-path signingkey confusion; Windows OpenSSH shipping without ssh-keygen -Y support.","solutions":["Read the embedded stderr — ssh-keygen states the exact failure (e.g. 'agent refused operation', 'no such file')","Ensure ssh-agent is running and the key is loaded: eval $(ssh-agent -s) && ssh-add <key>, then retry the commit","Verify user.signingkey (path or literal key) and that gpg.format=ssh plus gpg.ssh.program point to a working ssh-keygen","Upgrade OpenSSH to >= 8.0 if -Y sign is unsupported; test manually: ssh-keygen -Y sign -n git -f <key> file"],"exampleFix":"# reproduce the failure the commit path sees\nprintf test > /tmp/f && ssh-keygen -Y sign -n git -f ~/.ssh/id_ed25519 /tmp/f\n\n# fix: load the key into the agent the signer (-U) requires\neval \"$(ssh-agent -s)\"\nssh-add ~/.ssh/id_ed25519\ngit commit -S -m 'signed'","handlingStrategy":"try-catch","validationCode":"// pre-flight the signer exactly the way the commit path will use it\nfn ssh_sign_ok(key: &Path) -> bool {\n    std::process::Command::new(\"ssh-keygen\")\n        .args([\"-Y\", \"sign\", \"-n\", \"git\", \"-f\"]).arg(key)\n        .arg(tempfile::NamedTempFile::new().unwrap().path())\n        .stdin(Stdio::null()).output()\n        .map(|o| o.status.success()).unwrap_or(false)\n}","typeGuard":null,"tryCatchPattern":"// the message already carries ssh-keygen output — branch on its content\nmatch sign_buffer(&repo, &buffer).await {\n    Err(e) => {\n        let m = e.to_string();\n        if m.contains(\"agent refused\") || m.contains(\"no such identity\") {\n            return Err(e.context(\"run: ssh-add <your-key> (agent must hold the key for -U)\"));\n        }\n        Err(e)\n    }\n    s => s?,\n}","preventionTips":["Ensure SSH_AUTH_SOCK is set and the key is ssh-add'ed in the environment the app runs in (GUI launchers strip it)","Verify 'git config gpg.format' is ssh and user.signingkey resolves to an existing file or literal key","Smoke-test with ssh-keygen -Y sign -n git in CI before enabling signed commits"],"tags":["git","commit-signing","ssh","ssh-keygen","environment"],"backgroundTag":"git-commit-signing-failed","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}