Skip to content

fix(cli): openshell emits ANSI color into pipes and ignores NO_COLOR #3025

Description

@mrunalp

User Story

As an operator automating OpenShell from a shell script or supervisor loop,
I want openshell output to be plain text when it is not going to a terminal,
so that I can match on what a command reports without my patterns silently
failing against bytes I cannot see.

Problem Statement

Every openshell command colorizes its output unconditionally. The escape
sequences are emitted whether stdout is a terminal, a pipe, a file, or a
systemd journal, and no environment variable or flag turns them off.

openshell forward list is where this does the most damage, because its
STATUS column is the field most likely to be tested by automation:

SANDBOX      BIND    PORT     PID        STATUS$
openclaw-saw 0.0.0.0 8443     188500     ^[[32mrunning^[[39m$

The byte before running is m, closing ESC[32m. A pattern that expects
whitespace ahead of the status word — the natural way to write it against the
column layout — cannot match, and there is no indication why.

There is no way to suppress this. openshell forward list --help offers only
--gateway, --gateway-endpoint, --gateway-insecure, --workspace, and
-v. NO_COLOR has no effect: it appears nowhere in the CLI source.

Impact / Why This Matters

A pattern that can never match is indistinguishable from a condition that is
never true, so automation built on it fails silently and in the wrong direction.

This was found in a deployment where a supervisor loop polled
openshell forward list every 30 seconds to decide whether a port forward was
still alive. The guard was false on every iteration, so the loop's repair path —
forward stop followed by forward start — ran unconditionally against forwards
that were working. Every long-lived connection through each tunnel was severed on
a 30-second cadence. Measured at roughly 2,880 unnecessary rebuilds per forward
per day.

Two properties made it expensive to find. The failure is invisible in a terminal,
where the escapes render as color and the output looks correct. And it degrades
rather than breaks: short requests land between rebuilds and succeed, so only
long-lived connections show symptoms, which points investigation at the
application rather than at the shell guard. It went unnoticed for six days.

The available workarounds are all downstream of the defect:

  • Pipe through sed 's/\x1b\[[0-9;]*m//g' before matching. Works, but every
    caller has to know to do it, and nothing about the terminal output suggests
    it is needed.
  • Avoid the CLI and test the port directly. Sound advice for a liveness check
    specifically, but it does not help the general case of reading what a command
    reports.

Neither is discoverable from the CLI's own help output, which is where someone
hits this problem goes looking first.

This is not specific to forward list. Every table and status line the CLI
prints is affected, so any script parsing any openshell command has the same
latent bug. Log output has it too: with -v or RUST_LOG set, the log
formatter writes ANSI-decorated lines to stdout regardless of destination, so
anything capturing CLI logs for later parsing gets the same treatment.

Acceptance Criteria

  • openshell <command> 2>&1 | cat -A contains no escape sequences for any
    command, by default. This includes log output under -v or RUST_LOG,
    and error messages on stderr.
  • NO_COLOR set to any non-empty value disables color, including when
    stdout is a terminal (per https://no-color.org).
  • Output is still colorized when stdout is a terminal and no override
    applies, so interactive use is unchanged.
  • An explicit flag can force color on when stdout is redirected, for
    callers piping into a pager.
  • The flag governs every styled surface, not just tables — log lines,
    progress spinners, interactive prompts, and error rendering included.
    Several of these are drawn by libraries with independent color defaults.
  • Column alignment is identical with and without color — padding measures
    the text, not the text plus escapes.
  • The flag and the environment variables are documented.

Reproduction Steps

No gateway or sandbox is required:

  1. Ensure no gateways are registered (or point XDG_CONFIG_HOME at an empty
    directory).
  2. Run openshell gateway list 2>&1 | cat -A.
  3. Observe the escape sequences around the hint text:
    Register a gateway with: ^[[2mopenshell gateway add <endpoint>^[[0m
  4. Run NO_COLOR=1 openshell gateway list 2>&1 | cat -A and observe the same
    escapes — the variable has no effect.

For the log output:

  1. Run a command with logging on, against an endpoint that refuses connections:

    RUST_LOG=debug openshell sandbox list \
      --gateway test --gateway-endpoint http://127.0.0.1:1 | cat -A
  2. Observe ANSI-decorated log lines on stdout:
    ^[[2m2026-08-31T04:10:35.657579Z^[[0m ^[[34mDEBUG^[[0m ...

For the forward list case specifically:

  1. Create a sandbox and start a background forward:
    openshell forward start -d 8000 my-sandbox

  2. Run openshell forward list | cat -A and observe
    ^[[32mrunning^[[39m in the STATUS column.

  3. Confirm a whitespace-anchored pattern cannot match:

    openshell forward list \
      | grep -q '^my-sandbox[[:space:]].*[[:space:]]8000[[:space:]].*[[:space:]]running' \
      && echo MATCH || echo "NO MATCH"

    Prints NO MATCH. Inserting | sed 's/\x1b\[[0-9;]*m//g' before grep
    prints MATCH.

Environment

  • OpenShell: 0.0.109 (also present in earlier releases; see below)
  • OS: verified on Linux (Fedora). Not OS-specific — the colorization is
    unconditional in the CLI binary.
  • Deployment: not deployment-specific — reproduces with no gateway configured

The CLI has never gated colorization. owo-colors has been a dependency since
the initial commit and is declared without its supports-colors feature
(Cargo.toml:49), so .green() and friends emit escapes unconditionally with
no terminal detection. NO_COLOR does not appear in the CLI source. The
forward list status coloring specifically has been present since at least
ffc102a02 (2026-06-23).

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:cliCLI-related workstate:acceptedA maintainer decided OpenShell should pursue this issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions