fix(windows): sweep remaining unguarded text-mode subprocess sites codebase-wide

AST-driven pass over every subprocess.run/Popen/check_output/check_call/call
with text=True (or universal_newlines=True) and no explicit encoding=:
append encoding='utf-8', errors='replace' at the kwarg site. 136 call
sites across 28 files (cli.py, hermes_cli/main.py, tools_config.py,
environments, computer_use, gateway, scripts, skills helpers, agent/*).

Together with the salvaged #55339/#60741 commits this closes out issue
#53428's bug class; the salvaged #60751 linter rule in
check-windows-footguns.py now enforces it repo-wide (verified: 807 files
scanned, zero findings).
This commit is contained in:
teknium1 2026-07-24 10:05:18 -07:00 committed by Teknium
parent 051217342b
commit d4b867cf9f
28 changed files with 138 additions and 136 deletions

48
cli.py
View file

@ -1432,7 +1432,7 @@ def _git_repo_root() -> Optional[str]:
try:
result = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
capture_output=True, text=True, timeout=5,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5,
)
if result.returncode == 0:
return _normalize_git_bash_path(result.stdout.strip())
@ -1479,7 +1479,7 @@ def _resolve_worktree_base(repo_root: str) -> tuple:
def _git(args, timeout=20):
return subprocess.run(
["git", *args],
capture_output=True, text=True, timeout=timeout, cwd=repo_root,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout, cwd=repo_root,
)
# 1. Current branch's upstream, if it tracks one.
@ -1578,7 +1578,7 @@ def _setup_worktree(repo_root: str = None, sync_base: bool = True) -> Optional[D
try:
result = subprocess.run(
["git", "worktree", "add", str(wt_path), "-b", branch_name, base_ref],
capture_output=True, text=True, timeout=30, cwd=repo_root,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=30, cwd=repo_root,
)
if result.returncode != 0:
# If branching from the resolved remote ref failed for any reason
@ -1592,7 +1592,7 @@ def _setup_worktree(repo_root: str = None, sync_base: bool = True) -> Optional[D
base_ref, base_label = "HEAD", "HEAD (fallback — remote base failed)"
result = subprocess.run(
["git", "worktree", "add", str(wt_path), "-b", branch_name, base_ref],
capture_output=True, text=True, timeout=30, cwd=repo_root,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=30, cwd=repo_root,
)
if result.returncode != 0:
print(f"\033[31m✗ Failed to create worktree: {result.stderr.strip()}\033[0m")
@ -1673,7 +1673,7 @@ def _setup_worktree(repo_root: str = None, sync_base: bool = True) -> Optional[D
try:
subprocess.run(
["git", "worktree", "lock", "--reason", f"hermes pid={os.getpid()}", str(wt_path)],
capture_output=True, text=True, timeout=10, cwd=repo_root,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=10, cwd=repo_root,
)
logger.debug("Worktree locked: %s (pid=%s)", wt_path, os.getpid())
except Exception as e:
@ -1706,7 +1706,7 @@ def _worktree_has_unpushed_commits(worktree_path: str, timeout: int = 10) -> boo
try:
remote_refs = subprocess.run(
["git", "for-each-ref", "--format=%(refname)", "refs/remotes"],
capture_output=True, text=True, timeout=timeout, cwd=worktree_path,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout, cwd=worktree_path,
)
if remote_refs.returncode != 0:
return True
@ -1715,7 +1715,7 @@ def _worktree_has_unpushed_commits(worktree_path: str, timeout: int = 10) -> boo
result = subprocess.run(
["git", "log", "--oneline", "HEAD", "--not", "--remotes"],
capture_output=True, text=True, timeout=timeout, cwd=worktree_path,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout, cwd=worktree_path,
)
if result.returncode != 0:
return True
@ -1736,7 +1736,7 @@ def _worktree_is_dirty(worktree_path: str, timeout: int = 10) -> bool:
try:
result = subprocess.run(
["git", "status", "--porcelain"],
capture_output=True, text=True, timeout=timeout, cwd=worktree_path,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout, cwd=worktree_path,
)
if result.returncode != 0:
return True
@ -1769,7 +1769,7 @@ def _worktree_commits_all_merged_upstream(
try:
probe = subprocess.run(
["git", "rev-parse", "--verify", "--quiet", candidate],
capture_output=True, text=True, timeout=timeout, cwd=worktree_path,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout, cwd=worktree_path,
)
if probe.returncode == 0 and probe.stdout.strip():
base = candidate
@ -1782,7 +1782,7 @@ def _worktree_commits_all_merged_upstream(
try:
ahead = subprocess.run(
["git", "rev-list", "--count", f"{base}..HEAD"],
capture_output=True, text=True, timeout=timeout, cwd=worktree_path,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout, cwd=worktree_path,
)
if ahead.returncode != 0:
return False
@ -1794,7 +1794,7 @@ def _worktree_commits_all_merged_upstream(
cherry = subprocess.run(
["git", "cherry", base, "HEAD"],
capture_output=True, text=True, timeout=timeout, cwd=worktree_path,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout, cwd=worktree_path,
)
if cherry.returncode != 0:
return False
@ -1829,7 +1829,7 @@ def _worktree_lock_is_live(repo_root: str, worktree_path: str, timeout: int = 10
try:
result = subprocess.run(
["git", "worktree", "list", "--porcelain"],
capture_output=True, text=True, timeout=timeout, cwd=repo_root,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout, cwd=repo_root,
)
if result.returncode != 0:
return "live"
@ -1904,7 +1904,7 @@ def _cleanup_worktree(info: Dict[str, str] = None) -> None:
try:
subprocess.run(
["git", "worktree", "unlock", wt_path],
capture_output=True, text=True, timeout=10, cwd=repo_root,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=10, cwd=repo_root,
)
except Exception as e:
logger.debug("git worktree unlock failed (non-fatal): %s", e)
@ -1912,7 +1912,7 @@ def _cleanup_worktree(info: Dict[str, str] = None) -> None:
try:
subprocess.run(
["git", "worktree", "remove", wt_path, "--force"],
capture_output=True, text=True, timeout=15, cwd=repo_root,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=15, cwd=repo_root,
)
except Exception as e:
logger.debug("Failed to remove worktree: %s", e)
@ -1921,7 +1921,7 @@ def _cleanup_worktree(info: Dict[str, str] = None) -> None:
try:
subprocess.run(
["git", "branch", "-D", branch],
capture_output=True, text=True, timeout=10, cwd=repo_root,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=10, cwd=repo_root,
)
except Exception as e:
logger.debug("Failed to delete branch %s: %s", branch, e)
@ -2122,7 +2122,7 @@ def _prune_stale_worktrees(repo_root: str, max_age_hours: int = 24) -> None:
try:
subprocess.run(
["git", "worktree", "unlock", str(entry)],
capture_output=True, text=True, timeout=10, cwd=repo_root,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=10, cwd=repo_root,
)
except Exception as e:
logger.debug("Failed to unlock dead worktree %s: %s", entry.name, e)
@ -2131,13 +2131,13 @@ def _prune_stale_worktrees(repo_root: str, max_age_hours: int = 24) -> None:
try:
branch_result = subprocess.run(
["git", "branch", "--show-current"],
capture_output=True, text=True, timeout=5, cwd=str(entry),
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5, cwd=str(entry),
)
branch = branch_result.stdout.strip()
remove_result = subprocess.run(
["git", "worktree", "remove", str(entry), "--force"],
capture_output=True, text=True, timeout=15, cwd=repo_root,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=15, cwd=repo_root,
)
if remove_result.returncode != 0:
# Removal failed — keep the branch so any commits stay
@ -2150,7 +2150,7 @@ def _prune_stale_worktrees(repo_root: str, max_age_hours: int = 24) -> None:
if branch:
subprocess.run(
["git", "branch", "-D", branch],
capture_output=True, text=True, timeout=10, cwd=repo_root,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=10, cwd=repo_root,
)
logger.debug("Pruned stale worktree: %s (force=%s)", entry.name, force)
except Exception as e:
@ -2178,7 +2178,7 @@ def _prune_orphaned_branches(repo_root: str) -> None:
try:
result = subprocess.run(
["git", "branch", "--format=%(refname:short)"],
capture_output=True, text=True, timeout=10, cwd=repo_root,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=10, cwd=repo_root,
)
if result.returncode != 0:
return
@ -2191,7 +2191,7 @@ def _prune_orphaned_branches(repo_root: str) -> None:
try:
wt_result = subprocess.run(
["git", "worktree", "list", "--porcelain"],
capture_output=True, text=True, timeout=10, cwd=repo_root,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=10, cwd=repo_root,
)
for line in wt_result.stdout.split("\n"):
if line.startswith("branch refs/heads/"):
@ -2203,7 +2203,7 @@ def _prune_orphaned_branches(repo_root: str) -> None:
try:
head_result = subprocess.run(
["git", "branch", "--show-current"],
capture_output=True, text=True, timeout=5, cwd=repo_root,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5, cwd=repo_root,
)
current = head_result.stdout.strip()
if current:
@ -2227,7 +2227,7 @@ def _prune_orphaned_branches(repo_root: str) -> None:
try:
subprocess.run(
["git", "branch", "-D"] + batch,
capture_output=True, text=True, timeout=30, cwd=repo_root,
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=30, cwd=repo_root,
)
except Exception as e:
logger.debug("Failed to prune orphaned branches: %s", e)
@ -9528,7 +9528,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
from hermes_cli._subprocess_compat import windows_hide_flags
result = subprocess.run(
exec_cmd, shell=True, capture_output=True,
text=True, timeout=30, env=sanitized_env,
text=True, encoding="utf-8", errors="replace", timeout=30, env=sanitized_env,
# No console flash on Windows (#56747).
creationflags=windows_hide_flags(),
)