From 077f85a545049f89ae637aed5901549649270615 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 2 Jun 2026 22:18:30 +0200 Subject: [PATCH 1/2] Force ssh batch mode to avoid hidden auth prompts during fetch GIT_TERMINAL_PROMPT=0 only suppresses git's own credential prompts. When fetching over SSH, the authentication prompts (host key confirmation, password, key passphrase) are emitted by `ssh` directly to the controlling terminal, bypassing the piped stderr that we parse for progress. With the progress bars drawn on that same terminal, the prompt is hidden behind them and the fetch appears to hang indefinitely -- typically for users without a configured SSH key. Set GIT_SSH_COMMAND to append `-o BatchMode=yes` so ssh refuses to prompt and fails fast with a clear error instead, mirroring the GIT_TERMINAL_PROMPT treatment for git. Any user-provided GIT_SSH_COMMAND is preserved, and since ssh honors the first value seen for an option, an explicit BatchMode set by the user still takes precedence. Co-Authored-By: Claude Opus 4.8 --- src/git.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/git.rs b/src/git.rs index 98b1c32d..34c69be2 100644 --- a/src/git.rs +++ b/src/git.rs @@ -113,6 +113,16 @@ impl<'ctx> Git<'ctx> { // This ensures git fails immediately with a specific error message // instead of hanging indefinitely if auth is missing. cmd.env("GIT_TERMINAL_PROMPT", "0"); + + // `GIT_TERMINAL_PROMPT` only covers git's own prompts. SSH prompts + // (host key, password, passphrase) are emitted by `ssh` straight to the + // terminal, where they hide behind the progress bars and look like a + // hang. Force ssh into batch mode so it fails fast instead. Appended so a + // user's own `GIT_SSH_COMMAND` (and any `BatchMode` therein) still wins. + let mut ssh_command = + std::env::var("GIT_SSH_COMMAND").unwrap_or_else(|_| "ssh".to_string()); + ssh_command.push_str(" -o BatchMode=yes"); + cmd.env("GIT_SSH_COMMAND", ssh_command); let command = format_command(&cmd); log::info!("git: {:?} in {:?}", cmd, self.path); From 42874a820803def8ac868223fd3e640048315105 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 3 Jun 2026 15:38:53 +0200 Subject: [PATCH 2/2] recommend `--no-progress` and `--git-throttle` for interactive git operations Following review feedback that fully disabling input is too restrictive, soften the SSH handling and make failures actionable instead of silent: - Add `-o StrictHostKeyChecking=accept-new` so the common first-connection host-key prompt no longer blocks (changed keys are still rejected). This removes the most frequent prompt without compromising on key changes. - Detect authentication failures in git's stderr and emit a targeted help message (configure ssh-agent / a passphrase-less key, check repo access) rather than just dumping the raw stderr. Interactive credential prompts remain suppressed because they cannot be shown safely while fetching in parallel behind progress bars. Co-Authored-By: Claude Opus 4.8 Allow interactive auth with --no-progress instead of auto-accepting Per review feedback, drop the auto-accept of unknown host keys and instead make interactive authentication possible by turning progress bars off. Prompts from git and ssh are written directly to the controlling terminal, so they only collide with the progress bars visually; with the bars off the prompt is visible and answerable. Suppression of prompts is now conditional: - progress bars active, or no terminal at all (e.g. CI): suppress prompts (GIT_TERMINAL_PROMPT=0 + ssh BatchMode=yes) so the command fails fast instead of hanging on input nobody can see or provide. - interactive terminal with --no-progress: leave prompting untouched, so credentials and host keys can be entered normally. The auth-failure help message now tells the user to re-run with --no-progress to authenticate interactively (or to configure ssh-agent). Adds Diagnostics::progress_active() to expose whether bars are rendering. Co-Authored-By: Claude Opus 4.8 Recommend --git-throttle 1 alongside --no-progress in auth hint When authenticating interactively via --no-progress, concurrent fetches can still interleave their prompts on the shared terminal. Extend the auth-failure help to suggest `--git-throttle 1` so only one git operation prompts at a time. Co-Authored-By: Claude Opus 4.8 --- src/diagnostic.rs | 9 ++++++ src/git.rs | 70 +++++++++++++++++++++++++++++++++++++---------- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/src/diagnostic.rs b/src/diagnostic.rs index 403f4df5..a4ed9b8c 100644 --- a/src/diagnostic.rs +++ b/src/diagnostic.rs @@ -80,6 +80,15 @@ impl Diagnostics { *guard = multiprogress; } + /// Whether progress bars are currently being rendered. + /// + /// When this is true, interactive git/ssh prompts must be suppressed, since + /// they would be drawn over by the progress bars. When false (e.g. with + /// `--no-progress`), prompts can be shown and answered normally. + pub fn progress_active() -> bool { + Diagnostics::get().multiprogress.lock().unwrap().is_some() + } + /// Get the global diagnostics manager. fn get() -> &'static Diagnostics { GLOBAL_DIAGNOSTICS diff --git a/src/git.rs b/src/git.rs index 34c69be2..93deaa67 100644 --- a/src/git.rs +++ b/src/git.rs @@ -6,6 +6,7 @@ #![deny(missing_docs)] use std::ffi::OsStr; +use std::io::IsTerminal; use std::path::{Path, PathBuf}; use std::process::Stdio; use std::sync::Arc; @@ -109,20 +110,33 @@ impl<'ctx> Git<'ctx> { cmd.stdout(Stdio::piped()); cmd.stderr(Stdio::piped()); - // Disable interactive terminal prompts. - // This ensures git fails immediately with a specific error message - // instead of hanging indefinitely if auth is missing. - cmd.env("GIT_TERMINAL_PROMPT", "0"); - - // `GIT_TERMINAL_PROMPT` only covers git's own prompts. SSH prompts - // (host key, password, passphrase) are emitted by `ssh` straight to the - // terminal, where they hide behind the progress bars and look like a - // hang. Force ssh into batch mode so it fails fast instead. Appended so a - // user's own `GIT_SSH_COMMAND` (and any `BatchMode` therein) still wins. - let mut ssh_command = - std::env::var("GIT_SSH_COMMAND").unwrap_or_else(|_| "ssh".to_string()); - ssh_command.push_str(" -o BatchMode=yes"); - cmd.env("GIT_SSH_COMMAND", ssh_command); + // Interactive auth prompts (git's HTTP credentials, and ssh's host-key + // confirmation / password / passphrase) are written by git and ssh + // directly to the controlling terminal, bypassing the stdout/stderr we + // capture here. While progress bars are rendered, such a prompt is hidden + // behind them and the operation looks like it hangs forever. We therefore + // suppress prompts whenever progress bars are active and let the command + // fail fast instead; the error path below tells the user to re-run with + // `--no-progress` to authenticate interactively. + // + // Prompts are only allowed when there is genuinely someone to answer + // them: an interactive terminal with progress bars turned off + // (`--no-progress`). In every other case (bars active, or no terminal at + // all such as CI) we suppress prompts so the command fails fast instead + // of blocking on input nobody can see or provide. + let interactive = std::io::stderr().is_terminal(); + let prompts_suppressed = !interactive || crate::diagnostic::Diagnostics::progress_active(); + if prompts_suppressed { + // Make git fail instead of prompting for HTTP credentials. + cmd.env("GIT_TERMINAL_PROMPT", "0"); + // Make ssh fail instead of prompting (host key, password, passphrase). + // Appended so a user's own `GIT_SSH_COMMAND` settings take precedence + // (ssh uses the first value seen for an option). + let mut ssh_command = + std::env::var("GIT_SSH_COMMAND").unwrap_or_else(|_| "ssh".to_string()); + ssh_command.push_str(" -o BatchMode=yes"); + cmd.env("GIT_SSH_COMMAND", ssh_command); + } let command = format_command(&cmd); log::info!("git: {:?} in {:?}", cmd, self.path); @@ -197,8 +211,34 @@ impl<'ctx> Git<'ctx> { Some(code) => format!("exit code {}", code), None => String::from("unknown exit status"), }; + + // When prompts are suppressed (progress bars active), an auth failure + // surfaces here as a hard error rather than a hidden prompt. Detect + // it and tell the user how to authenticate interactively instead of + // just dumping git's stderr. + let lower = collected_stderr.to_lowercase(); + let is_auth_failure = lower.contains("permission denied") + || lower.contains("authentication failed") + || lower.contains("could not read username") + || lower.contains("host key verification failed"); + + let help = if is_auth_failure && prompts_suppressed { + format!( + "Authentication failed. Interactive prompts (credentials and host-key \ + confirmation) are disabled while progress bars are shown, because they \ + cannot be displayed safely during a parallel fetch. Re-run with \ + `--no-progress` to authenticate interactively; also pass `--git-throttle 1` \ + so prompts from concurrent fetches don't interleave on the terminal. \ + Alternatively, configure access non-interactively (e.g. add your key to \ + `ssh-agent`).\n\ngit stderr:\n{}", + collected_stderr + ) + } else { + format!("git failed with stderr output:\n{}", collected_stderr) + }; + Err(err!( - help = format!("git failed with stderr output:\n{}", collected_stderr), + help = help, "Git command `{}` failed in directory {:?} with {}.", command, self.path,