Found while sweeping for copies of #374. Same genus as #377 but a different guard and a different fix, so filed separately: this call does not route through _run_git.
verify.run_verify_commands runs the operator's policy verify commands and decodes their output strictly:
try:
proc = subprocess.run( # nosec B602
command,
shell=True,
cwd=cwd,
capture_output=True,
text=True,
timeout=COMMAND_TIMEOUT_S,
)
output = (proc.stdout + proc.stderr)[-2000:]
results.append(CommandResult(command, proc.returncode, output))
except subprocess.TimeoutExpired:
results.append(CommandResult(command, -1, "timed out"))
except subprocess.TimeoutExpired is the entire net. A UnicodeDecodeError raised while decoding the child's output escapes it.
Why this is the most likely of the family to fire
The decoded bytes are arbitrary test-suite output, not a path — core.quotePath offers nothing here. Any non-UTF-8 byte anywhere in stdout/stderr does it: a latin-1 fixture filename appearing in a traceback, a bytes repr in an assertion diff, a compiler or linter diagnostic in a non-UTF-8 locale.
Consequence
The exception escapes before any results entry is appended, so the whole list is lost — not just the offending command. It propagates run_verify_commands → verify_commands_outcome → _dev_phase → engine.py, and a deterministic verify gate is reported as a run crash with UnicodeDecodeError, pointing nowhere near the test suite that actually produced the byte. Re-running reproduces it forever, because the offending output is deterministic.
Suggested fix
errors="replace" on this call. The output is captured purely to show a human/repair session what failed ([-2000:], i.e. already lossy), so mojibake on one line is strictly better than losing the run — and unlike the _run_git sites, nothing here is fed back to a filesystem API, so surrogate round-tripping is not needed. tui/launch.py:439 already uses errors="replace" for the same reason and is a good model.
Pre-existing; not introduced by #373 or #376.
Found while sweeping for copies of #374. Same genus as #377 but a different guard and a different fix, so filed separately: this call does not route through
_run_git.verify.run_verify_commandsruns the operator's policy verify commands and decodes their output strictly:except subprocess.TimeoutExpiredis the entire net. AUnicodeDecodeErrorraised while decoding the child's output escapes it.Why this is the most likely of the family to fire
The decoded bytes are arbitrary test-suite output, not a path —
core.quotePathoffers nothing here. Any non-UTF-8 byte anywhere in stdout/stderr does it: a latin-1 fixture filename appearing in a traceback, abytesrepr in an assertion diff, a compiler or linter diagnostic in a non-UTF-8 locale.Consequence
The exception escapes before any
resultsentry is appended, so the whole list is lost — not just the offending command. It propagatesrun_verify_commands→verify_commands_outcome→_dev_phase→engine.py, and a deterministic verify gate is reported as a run crash withUnicodeDecodeError, pointing nowhere near the test suite that actually produced the byte. Re-running reproduces it forever, because the offending output is deterministic.Suggested fix
errors="replace"on this call. The output is captured purely to show a human/repair session what failed ([-2000:], i.e. already lossy), so mojibake on one line is strictly better than losing the run — and unlike the_run_gitsites, nothing here is fed back to a filesystem API, so surrogate round-tripping is not needed.tui/launch.py:439already useserrors="replace"for the same reason and is a good model.Pre-existing; not introduced by #373 or #376.