headroomlabs-ai/headroom · error · RuntimeError

`{' '.join(cmd)}` failed (exit {result.returncode}): {stderr

Error message

`{' '.join(cmd)}` failed (exit {result.returncode}):
{stderr_snippet}

What it means

Raised after the CLI subprocess completed but exited non-zero: result.returncode != 0. The RuntimeError embeds the exit code plus the first _MAX_SNIPPET_LEN chars of stderr, so the actual CLI failure (auth error, quota, bad flags, model access denied) appears in the message. headroom treats any non-zero exit as a failed analysis.

Source

Thrown at headroom/learn/analyzer.py:615

            ) from None
        cmd = shim_cmd
        try:
            result = run(cmd, input=prompt, capture_output=True, text=True, timeout=hard_cap)
        except FileNotFoundError:
            raise RuntimeError(
                f"`{cmd[0]}` not found in PATH. Install it or use a different backend "
                "with --model <litellm-model-name>."
            ) from None
    except subprocess.TimeoutExpired:
        raise RuntimeError(
            f"`{' '.join(cmd)}` did not respond within {hard_cap}s. "
            "Check network connectivity, raise HEADROOM_LEARN_CLI_TIMEOUT_SECS, "
            "or try a different backend with --model <litellm-model-name>."
        ) from None

    if result.returncode != 0:
        stderr_snippet = (result.stderr or "")[:_MAX_SNIPPET_LEN]
        raise RuntimeError(
            f"`{' '.join(cmd)}` failed (exit {result.returncode}):\n{stderr_snippet}"
        )

    # Log stderr warnings even on success (auth refreshes, deprecation notices).
    if result.stderr and result.stderr.strip():
        logger.debug("CLI stderr (exit 0): %s", result.stderr[:_MAX_SNIPPET_LEN])

    try:
        return _strip_fenced_json(result.stdout)
    except json.JSONDecodeError as exc:
        stdout_snippet = (result.stdout or "")[:_MAX_SNIPPET_LEN]
        raise RuntimeError(
            f"`{' '.join(cmd)}` returned unparseable output. "
            f"First {_MAX_SNIPPET_LEN} chars:\n{stdout_snippet}"
        ) from exc


def _call_claude_cli_streaming(

View on GitHub (pinned to 322425c43b)

Solutions

  1. Read the embedded stderr snippet — it names the actual CLI failure; fix that first (re-auth with `claude login` / `gemini auth` etc.)
  2. Update headroom (its cmd_parts may be fixed for newer CLIs) and/or update the CLI to a compatible version
  3. Test the CLI standalone with a trivial prompt to isolate auth vs. flag problems
  4. Fall back to an API model: export a key and run headroom learn --model <litellm-model-name>

Example fix

# before
headroom learn  # `claude ...` failed (exit 1): authentication expired

# after
claude login  # or: gemini auth apply
headroom learn
Defensive patterns

Strategy: try-catch

Validate before calling

import subprocess
r = subprocess.run([cli, '--version'], capture_output=True, text=True)
if r.returncode != 0:
    raise SystemExit(f'{cli} unhealthy (exit {r.returncode}): {r.stderr[:200]} — re-auth before headroom learn')

Try / catch

try:
    recs = run_learn()
except RuntimeError as e:
    if 'failed (exit' in str(e):
        logger.error('CLI backend failed: %s', e)
        os.environ.pop('HEADROOM_LEARN_CLI', None)  # try API backend next
        recs = run_learn()
    else:
        raise

Prevention

When it happens

Trigger: `headroom learn` invokes the CLI backend and it exits non-zero: expired/invalid CLI credentials, plan limits, the CLI rejecting the piped prompt flags (e.g. unsupported --model or output-format flags after a CLI update), or sandbox/permission denials inside the CLI itself.

Common situations: Claude/Gemini/Codex CLI login token expired since last use; CLI auto-updated and changed flag semantics so headroom's hardcoded cmd_parts no longer work; org policy/VPN blocking the CLI's API; quota exhausted mid-run.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/654d658eccccf144. Report an issue: GitHub.