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

  1. 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.
  2. Upgrade or pin gh to a version that supports every requested --json field (check gh --version; field lists drift across releases).
  3. Remove any gh wrapper/alias/function in PATH or shell rc that writes to stdout.
  4. 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

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

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/8a36eca934a8bf33. Report an issue: GitHub.