The GitHub auth pre-check, with no gh auth
gh auth used to hand
straight to git clone, whose interactive Username for 'https://github.com':
prompt blocked on the stdin that crossterm owns in raw mode — the whole TUI froze with no escape.
The fix runs gh auth status as a pre-check (GitHub hosts only, 5 s timeout,
fails closed) before any git. Logged out, it shows an inline amber
“GitHub auth required” prompt with Enter=Retry · s=Skip · Esc=Back — and Esc
drops you back to a fully responsive picker.
The recording — driven headless against a logged-out gh
This is the real ainb binary in a scripted PTY (vhs), with a stubbed
gh on PATH that exits non-zero exactly like gh auth status when
signed out. Watch: Enter on the GitHub favorite → the inline auth prompt appears (no credential
hang) → Esc returns to the picker.
gh auth login && gh auth setup-git · Enter=Retry s=Skip Esc=BackWhat happens, step by step
The whole pre-check sits between “you picked a repo” and “git runs”. Expand each step.
1 · You pick a remote GitHub repo pick_repo.rs · resolve_outcome
Enter on a GithubShorthand / HttpsUrl row (or smart-parsing a pasted URL)
resolves to PickRepoOutcome::StartClone(source). Local paths and SSH sessions skip
straight to Configure — they never need a clone.
2 · The host gate decides if gh is even relevant events.rs · StartClone
Only GitHub sources are routed through the check: a GithubShorthand, or an
HttpsUrl whose parsed host is github.com (via
repo_source::is_github_host). A GitLab / Bitbucket / self-hosted HTTPS remote is left
alone — the gh auth status probe is GitHub-specific and would otherwise put an
irrelevant failure screen in front of those clones. (CodeRabbit finding #1.)
3 · The pre-check runs gh auth status, bounded state.rs · check_git_auth
On a blocking thread, with a tokio::time::timeout(5s) wrapper. A hung gh
(network stall, wedged credential helper) can’t freeze the picker — both the timeout and a task
panic fail closed to “not authenticated” and log a warning.
(CodeRabbit finding: add a timeout.)
4 · Logged out → inline amber prompt, not a git credential hang pick_repo.rs · GitAuthStatus::NotAuthenticated
The picker renders, under the highlighted row,
🔑 GitHub auth required. In another terminal run: /
gh auth login && gh auth setup-git /
Enter=Retry s=Skip auth Esc=Back. Because the check ran
before git, git clone’s Username for… stdin prompt never happens.
5 · You recover — Retry, Skip, or Esc pick_repo.rs · handle_key
Enter re-runs the check (e.g. after you authenticate in another terminal). s skips the
gate and proceeds to clone anyway — and even then git can’t hang, because every network-facing git
call sets GIT_TERMINAL_PROMPT=0 / GIT_ASKPASS=echo and fails fast.
Esc dismisses the prompt and returns to a responsive picker.
The code that matters
// git/repo_source.rs — only github.com routes through `gh`. pub fn is_github_host(https_url: &str) -> bool { url::Url::parse(https_url) .ok() .and_then(|u| u.host_str() .map(|h| h.eq_ignore_ascii_case("github.com"))) .unwrap_or(false) // fails closed on junk URLs }
// app/events.rs — StartClone gate. let needs_auth_check = match &source { RepoSource::GithubShorthand { .. } => true, RepoSource::HttpsUrl(url) => is_github_host(url), _ => false, // SSH, local, non-GitHub HTTPS: skip };
// app/state.rs — bounded probe, fails closed. let auth_ok = match tokio::time::timeout( Duration::from_secs(5), auth_check).await { Ok(Ok(ok)) => ok, Ok(Err(e)) => { tracing::warn!(?e, "join error"); false } Err(_) => { tracing::warn!("gh auth timed out"); false } };
Username for /
Password for on the pane.How it’s proven — two independent ways
A tmux tripwire driving the real binary tests/tripwire_new_session_github_auth_no_gh.rs
Launches ainb in a detached tmux session with a stubbed logged-out gh on
PATH, presses Enter on the GitHub favorite, and asserts: (1) the inline
auth required + gh auth login prompt renders; (2) the pane never
contains Username for / Password for; (3) Esc returns to the picker
(Enter=Select back, prompt gone). Passes green locally in ~11 s.
The recording above vhs · auth-precheck.tape
Same flow, same stubbed gh, captured as a deterministic GIF/MP4 so the behaviour is
visible, not just asserted. The .tape is the source of truth and re-runs identically.
Gotchas worth knowing
- The prompt renders under the highlighted row. It surfaces when you Enter on a remote favorite row. Typing a brand-new URL that filters every row out leaves no row to anchor it — a known edge worth a follow-up, called out separately.
- The GitHub-specific copy is intentional. The host gate guarantees this state only ever renders for GitHub sources, so a generic “configure credentials” branch would be dead code. The invariant is documented in the source.
- Skip (
s) is still safe. It proceeds to clone, butGIT_TERMINAL_PROMPT=0on every network-facing git call means git fails fast instead of blocking on stdin. - Fails closed. Timeout, task panic, unparseable URL — all resolve to “not authenticated”, never a silent fall-through to git.
FAQ
- How do I try this myself without logging out of
gh? - Run
ainbwith aPATHwhose first entry holds a fakeghthatexit 1s — or justgh auth logout— then Enter on a GitHub repo in the picker. - Does a non-GitHub HTTPS remote get blocked?
- No.
is_github_hostonly matchesgithub.com; GitLab / Bitbucket / self-hosted HTTPS clones skip theghprobe entirely. - What if
ghhangs? - The 5-second timeout fires, logs a warning, and treats it as not-authenticated — the picker can’t get
stuck on
Checking. - What was the actual bug?
git cloneover HTTPS with no credentials printsUsername for 'https://github.com':and waits on stdin. In raw mode crossterm owns stdin, so that prompt was invisible and unanswerable — the TUI just froze. The pre-check makes git never get there.