Skip to content

Clean non-interactive stdout: split UI chrome to stderr, or add a structured output mode #3397

@baiwei0427

Description

@baiwei0427

Summary

In non-interactive mode (-p / stdin), copilot writes a mix of model output, interactive UI chrome (Braille spinner glyphs), and tool-execution annotations to stdout. Even with --no-color, the stdout stream is not safe to pipe to a file or to a downstream parser without significant filtering.

This makes the CLI hard to use as a clean input-to-stdout primitive in CI pipelines, scripts, and wrappers.

Examples observed today

Running:

copilot --model gpt-5.5 --effort low --no-color -p "Run 'cat /tmp/x' then summarise" --add-dir /tmp

…produces stdout like:

⣾⣽⣻⢿● Display contents of /tmp/x (shell)
  │ cat /tmp/x
  └ 4 lines...

The file contains three short lines describing X.

So a CI step that does copilot ... > review.md ends up with a review.md that contains:

  • a Braille-spinner glyph at the start of the first byte
  • ● <description> (toolname) annotation lines
  • / tool-call body indicator lines
  • the actual model response

To get clean output today we have to:

copilot ... \
  | LC_ALL=C sed -E 's/(\xe2\xa0[\x80-\xbf]|\xe2\xa1[\x80-\xbf]|\xe2\xa2[\x80-\xbf]|\xe2\xa3[\x80-\xbf])+//g; s/\x1b\[[0-9;?]*[A-Za-z]//g; s/\r//g' \
  | sed -E '/^● .*\([a-zA-Z_-]+\)$/d; /^  │ /d; /^  └ /d; /^  ├ /d; /^  ─ /d' \
  | awk 'NF{p=1} p'

This is brittle: any change to the spinner glyph set, annotation prefix, or indentation in a future CLI release silently breaks the filter (or worse, drops legitimate review content that happens to match the prefixes).

The footer summary (Changes / AI Units / Tokens) is correctly written to stderr, which is great — please keep doing that. The request is to give the same treatment to the spinner and tool annotations.

Request

Add a clean structured-output mode for non-interactive runs. Any of these would help, in increasing order of value:

Option A — minimum: split chrome to stderr

Send spinner glyphs, tool-call annotations (● …, │ …, └ …), and any other "what I'm doing" UI to stderr, not stdout. Stdout then contains only the model's final response text. This is by far the smallest change and would already make every CI integration much simpler.

Option B — better: a --quiet flag

A flag like --quiet (or --no-progress) that explicitly suppresses all UI chrome from both streams when set, leaving only the model response on stdout. Many CLIs have this (gh --quiet, git --quiet).

Option C — best: a --output json flag

Structured output describing the run, e.g.:

{
  "model": "gpt-5.5",
  "response": "<the model's actual text>",
  "tool_calls": [
    {"name": "shell", "command": "cat /tmp/x", "stdout": "...", "exit_code": 0}
  ],
  "usage": {"input_tokens": 12345, "output_tokens": 67, "reasoning_tokens": 64},
  "exit_reason": "completed"
}

This would let CI integrators reliably distinguish "what the model said" from "what tools it ran" without any string parsing, and would compose well with jq.

Why this matters

We just shipped a self-hosted GitHub Actions workflow that runs two Copilot CLI invocations per PR (GPT and Claude reviews) and synthesises a consensus review (#3396 was filed during the same effort). Every other CI integrator hitting this needs to reinvent the same sed/awk filter, and any of us silently regress when the CLI's UI evolves.

A documented, stable contract for non-interactive output would also unlock cleaner downstream tooling (linters, gates, dashboards) built on top of Copilot CLI.

Environment

  • GitHub Copilot CLI: 1.0.49
  • Linux (Ubuntu 22.04 on WSL2)
  • Use case: non-interactive -p/stdin, output captured to a file in a GitHub Actions step

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions