odysseus-dev/odysseus · error · RuntimeError
gh returned invalid JSON: {exc}
Error message
gh returned invalid JSON: {exc} What it means
Raised by _run_gh_json in scripts/pr_blocker_audit.py when the gh subprocess exits 0 but its stdout is not valid JSON. Since every gh invocation uses --json/--api with machine output, any non-JSON stdout means the wrong thing answered: gh printed human text, a wrapper/proxy injected content, or stdout got truncated/interleaved.
Source
Thrown at scripts/pr_blocker_audit.py:259
if warnings:
return {"items": payload, "warnings": warnings}
return payload
def _fetch_live_pr_files(repo: str, number: int) -> list[str]:
api_path = f"repos/{repo}/pulls/{number}/files?per_page=100"
payload = _run_gh_json(["gh", "api", "--paginate", api_path])
return _extract_files(payload)
def _run_gh_json(cmd: list[str]):
result = subprocess.run(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
if result.returncode != 0:
raise RuntimeError(result.stderr.strip() or f"{cmd[0]} exited with {result.returncode}")
try:
return json.loads(result.stdout or "[]")
except json.JSONDecodeError as exc:
raise RuntimeError(f"gh returned invalid JSON: {exc}") from exc
def normalize_prs(payload) -> list[PullRequest]:
raw_prs = payload.get("items", []) if isinstance(payload, dict) else payload
if raw_prs is None:
raw_prs = []
if not isinstance(raw_prs, list):
raise ValueError("expected input JSON to be a list of pull requests or an object with an items list")
return [normalize_pr(item) for item in raw_prs if isinstance(item, dict)]
def missing_file_metadata_count(prs: list[PullRequest]) -> int:
return sum(1 for pr in prs if not pr.files)
def missing_metadata_warning(count: int) -> str:
noun = "PR" if count == 1 else "PRs"
return f"Warning: {count} {noun} still missing changed-file metadata."View on GitHub (pinned to f9235ebbf1)
Solutions
- Run the failing gh command manually and inspect stdout: gh pr list --repo <repo> --state open --json number,title,... — look for text before/around the JSON.
- Upgrade or pin gh to a version that supports every requested --json field (check gh --version; field lists drift across releases).
- Remove any gh wrapper/alias/function in PATH or shell rc that writes to stdout.
- Check for proxy/HTML injection: curl -s https://api.github.com and confirm clean JSON responses.
Example fix
# before $ gh pr list --repo org/repo --json files,mergeStateStatus # 'files' unsupported in old gh gh: unknown JSON field # rc 0 in some wrappers -> python: gh returned invalid JSON # after $ brew upgrade gh # or use a field list valid for your gh version $ gh --version
Defensive patterns
Strategy: validation
Validate before calling
import json, subprocess
def run_gh_json_checked(cmd: list[str]):
r = subprocess.run(cmd, text=True, capture_output=True, check=False)
out = r.stdout or '[]'
try:
return json.loads(out)
except json.JSONDecodeError:
preview = out[:200].replace('\n', ' ')
raise RuntimeError(f'{cmd[0]} stdout not JSON (rc={r.returncode}): {preview!r}') Type guard
def looks_like_gh_json(stdout: str) -> bool:
s = stdout.lstrip()
return s.startswith(('[', '{')) Try / catch
try:
prs = fetch_live_prs(repo)
except RuntimeError as e:
if 'invalid JSON' in str(e):
# gh wrapper/environment is polluting stdout; run gh bare in a clean shell to compare
raise RuntimeError(f'gh stdout corrupted; check PATH wrappers/shell rc: {e}')
raise Prevention
- Never wrap gh in scripts/functions that print to stdout.
- Keep --json field lists synced to your installed gh version (check with gh pr list --json help or docs).
- Run subprocess with stdout/stderr as separate pipes (as this script does) and reject output that does not start with '[' or '{'.
- Pin the gh version in CI to avoid field-list drift.
When it happens
Trigger: gh pr list ... --json <fields> succeeding but printing a deprecation notice mixed into stdout; a gh wrapper script or shell profile echoing text; a transparent proxy returning an HTML interstitial; output truncation when --paginate concatenates pages without valid separation; gh version change where a field name errors are printed but rc stays 0.
Common situations: A gh shim/alias in PATH that adds logging; shell rc files printing text that leaks into captured stdout when gh is invoked via a login shell; gh version where one of the requested --json fields no longer exists; running through a proxy appliance.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- invalid JSON in {path}: {exc.msg} at line {exc.lineno}, colu
- {cmd[0]} exited with {result.returncode}
- expected input JSON to be a list of pull requests or an obje
- could not read {path}: {exc}
- HTTP ${res.status}
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/8a36eca934a8bf33.
Report an issue: GitHub.