fix(git): never block internal git calls on credential prompts

Port from openai/codex#34540 / #34612 ("detach non-interactive
subprocesses from stdin"): internal git invocations that run with nobody
attached — MCP catalog installs, plugin install/update, profile
distribution staging, worktree base fetches, and the desktop review
pane's git/gh backend — could hang on a credential prompt when a remote
is private, misconfigured, or requires auth. git prompts on the
inherited terminal (or via Git Credential Manager on Windows), so the
operation silently waits until its timeout, or forever at sites without
one (mcp_catalog clones have no timeout at all and inherit the parent
terminal).

- Add noninteractive_git_env() to hermes_cli/_subprocess_compat.py:
  GIT_TERMINAL_PROMPT=0 + GCM_INTERACTIVE=Never on a copy of the
  environment; GIT_ASKPASS/SSH_ASKPASS deliberately preserved so
  working non-interactive auth still succeeds.
- Wire it + stdin=DEVNULL into: mcp_catalog._do_git_install (clone/
  checkout), plugins_cmd (clone + pull), profile_distribution._git_clone,
  web_git._git/_gh (gh also gets GH_PROMPT_DISABLED=1), and cli.py's
  worktree base fetch helper.
- Tests: env contract, a real-git E2E against a local 401 Basic-auth
  HTTP server proving fail-fast ("terminal prompts disabled") instead
  of a hang, and per-call-site plumbing assertions. Sabotage-verified:
  removing the env from web_git._git fails the site test.
This commit is contained in:
Teknium
2026-07-28 17:16:43 -07:00
parent ded2314910
commit 58708c7066
7 changed files with 314 additions and 5 deletions
+18 -2
View File
@@ -37,6 +37,7 @@ from typing import Any, Dict, List, Optional
import yaml
from hermes_constants import get_hermes_home, get_optional_mcps_dir
from hermes_cli._subprocess_compat import noninteractive_git_env
from hermes_cli.colors import Colors, color
from hermes_cli.config import (
load_config,
@@ -412,9 +413,16 @@ def _do_git_install(entry: CatalogEntry) -> Path:
# SHA ref before we fall back to full-clone-then-checkout).
is_sha_ref = bool(re.fullmatch(r"[0-9a-f]{7,40}", install.ref))
# Never let an install hang on a credential prompt: catalog installs run
# from CLI commands and dashboard flows where nobody can answer git's
# username/password prompt (private repo, bad remote, auth required).
_git_env = noninteractive_git_env()
if not is_sha_ref:
proc = subprocess.run(
[git, "clone", "--depth", "1", "--branch", install.ref, install.url, str(dest)],
stdin=subprocess.DEVNULL,
env=_git_env,
)
if proc.returncode == 0:
pass
@@ -426,10 +434,18 @@ def _do_git_install(entry: CatalogEntry) -> Path:
is_sha_ref = True # treat the same as a SHA ref from here
if is_sha_ref:
proc = subprocess.run([git, "clone", install.url, str(dest)])
proc = subprocess.run(
[git, "clone", install.url, str(dest)],
stdin=subprocess.DEVNULL,
env=_git_env,
)
if proc.returncode != 0:
raise CatalogError(f"git clone failed for {install.url}")
proc = subprocess.run([git, "-C", str(dest), "checkout", install.ref])
proc = subprocess.run(
[git, "-C", str(dest), "checkout", install.ref],
stdin=subprocess.DEVNULL,
env=_git_env,
)
if proc.returncode != 0:
raise CatalogError(f"git checkout {install.ref} failed")