Skip to content

What is the NO_COLOR convention?

NO_COLOR is a convention in which a command-line tool turns off colored output when the NO_COLOR environment variable is not empty, keeping logs plain.

Last updated , 8 min read

What is the NO_COLOR convention?

The NO_COLOR convention is an informal standard under which command-line programs add no color when the NO_COLOR environment variable is present and not empty. Any value counts, so NO_COLOR=1 and NO_COLOR=false both turn color off. A user can add export NO_COLOR=1 to a shell profile to turn off color in every program that follows it.

Terminal colors are ANSI escape codes, bytes that a terminal turns into formatting. Before the convention, each program had its own way to turn them off, and some had none. The NO_COLOR website asks software that adds color by default to check the variable. Software that adds color only when a user asks for it does not need to.

Color is part of the output of a command-line interface (CLI). Testing a command-line application therefore includes runs with NO_COLOR set and unset, because the two runs can write different bytes.

How does NO_COLOR work?

A program that adds color by default runs a short series of checks before it writes to a stream. A common order, from the highest precedence to the lowest, is:

  1. The program reads an explicit choice, e.g. a --color=never flag. The NO_COLOR website says flags and user config files should override the variable.
  2. The program reads NO_COLOR. If it is present and not empty, the program adds no color.
  3. The program reads a variable that forces color, e.g. FORCE_COLOR. If it is present and not empty, the program adds color even when output goes to a pipe.
  4. The program reads TERM. The value dumb means the terminal cannot format text, so the program adds no color.
  5. The program checks whether the stream is a TTY, a terminal device it can detect, and adds color only if it is.
How a program decides to add color Color flag or config set? NO_COLOR present and not empty? FORCE_COLOR present and not empty? TERM is dumb? Stream is a terminal (TTY)? no no no no no yes yes yes yes yes Follow the flag or config No color Color No color Color No color
The first check that answers yes decides the output. Tools disagree about whether NO_COLOR or FORCE_COLOR comes first.

The Command Line Interface Guidelines list the same signals for turning color off, with --no-color as the flag. They also advise checking stdout and stderr separately, so warnings on stderr can keep their color while stdout goes to a pipe.

When color is on, the program writes sequences that ECMA-48 calls Select Graphic Rendition. Each one starts with the escape byte and [, holds numbers separated by semicolons, and ends with m. \033[33m turns text yellow, \033[1m makes it bold, and \033[0m resets both. A terminal applies each sequence and hides it. A log file keeps the bytes, which cat -v shows as ^[[33m.

What is an example of NO_COLOR?

Here is an illustrative example. Acme Co. sells furniture online, and its command-line tool, acme, exports orders with acme export and prints warnings to stderr in yellow. A developer at Acme asks a coding agent to "Add a --color option to acme export that takes auto, always, or never." The work takes five steps:

  1. The agent adds the option to the use_color() function below, after the NO_COLOR check.
  2. The agent's test, run where NO_COLOR is unset, pipes acme export --color=always, finds \033[33m on stderr, and passes.
  3. A developer with NO_COLOR=1 in a shell profile runs acme export --color=always 2>&1 | less -R and sees no yellow, although the convention lets a flag win.
  4. The developer writes the test below to pin the order.
  5. The agent moves the option check to the top, and all three cases pass.

Python's os.environ.get() returns an empty string for an empty variable, which counts as false, so an empty NO_COLOR counts as unset:

def use_color(option, stream):
    if os.environ.get("NO_COLOR"):
        return False
    if option != "auto":
        return option == "always"
    if os.environ.get("FORCE_COLOR"):
        return True
    if os.environ.get("TERM") == "dumb":
        return False
    return stream.isatty()

The test drops inherited variables whose names end in _COLOR, so a local setting cannot change the result. Its second case records Acme's choice that NO_COLOR wins over FORCE_COLOR:

import os
import subprocess
import pytest

@pytest.mark.parametrize("env_vars, option, colored", [
    ({"NO_COLOR": "1"}, "--color=always", True),
    ({"NO_COLOR": "1", "FORCE_COLOR": "1"}, "--color=auto", False),
    ({"NO_COLOR": "", "FORCE_COLOR": "1"}, "--color=auto", True),
])
def test_color_precedence(env_vars, option, colored):
    env = {k: v for k, v in os.environ.items() if not k.endswith("_COLOR")}
    env.update(env_vars)
    cmd = ["acme", "export", option]
    result = subprocess.run(cmd, capture_output=True, text=True, env=env)
    assert ("\033[" in result.stderr) == colored

Before step 5, only the first case fails, because use_color() returns before reading the option. This example is simplified. A real CLI would also need cases under a pseudoterminal, where auto turns color on.

What changes when a coding agent writes the code?

Some coding agents set color variables for the commands they start, e.g. Codex sets NO_COLOR=1 and TERM=dumb, even for a command it runs in a pseudoterminal. A program that follows the convention then writes plain output to the agent, whatever its TTY check returns. The colored path runs mainly when a person types the command.

A test the agent writes can then pass in the agent's plain environment and fail where color returns. A continuous integration (CI) job in GitHub Actions has no terminal, but a team can set FORCE_COLOR=1 there to keep color in the log. A golden file that the agent recorded then fails on escape codes around each warning, one way tests pass locally and fail in CI. Code that ignores NO_COLOR also sends escape codes into the agent's context when the agent runs it in a pseudoterminal.

A practical adjustment is to set NO_COLOR, FORCE_COLOR, and TERM inside each CLI test instead of inheriting them, with one case for each rule in the precedence order. An agent-native CLI can also accept --color=never, so a caller does not depend on the environment.

What are the limits of NO_COLOR?

NO_COLOR has four limits:

  • It covers only color. The NO_COLOR FAQ answers no when asked whether the variable should turn off bold, underline, or italic text. Those styles use the same sequences, so a snapshot test of help and version output can still hold escape codes. A tool that must write plain text can offer a separate flag that removes every style.
  • It reaches only programs that check it. A program that never reads the variable still writes codes, and nothing enforces the convention.
  • It passes to child processes. A value exported in a shell or a CI job also reaches tests that expect color.
  • It does not clean what is already written. Codes already in a log stay there.

A regular expression removes color and style codes from a saved log:

perl -pe 's/\e\[[0-9;]*m//g' build.log > build-plain.log

The pattern matches only sequences that end in m. Cursor movement codes end in other letters, e.g. A to move up, so they need a wider pattern.

How is NO_COLOR different from FORCE_COLOR?

FORCE_COLOR is the opposite convention. When the FORCE_COLOR variable is present and not empty, a program adds color even when the stream is not a terminal. NO_COLOR removes color that a program adds by default, and FORCE_COLOR adds color that a TTY check would remove.

When both are set, tools disagree. Python's documentation says NO_COLOR takes precedence over FORCE_COLOR. Node.js ignores NO_COLOR when FORCE_COLOR holds a supported value, e.g. 1, and turns color off for any other value. Its supported values include an empty string, which the FORCE_COLOR convention treats as unset. Like Node.js, the sample code on the FORCE_COLOR site lets FORCE_COLOR win.

The older CLICOLOR convention uses CLICOLOR to ask for color on a terminal and CLICOLOR_FORCE to ask for it everywhere. Its page marks it as deprecated in favor of NO_COLOR and FORCE_COLOR, and it lets NO_COLOR override both of its variables.

FAQs

Does NO_COLOR need a value?

The NO_COLOR variable needs a value that is not empty, and any value works. NO_COLOR=1 and NO_COLOR=false both turn color off in programs that follow the convention, and an empty NO_COLOR counts as unset.

Should NO_COLOR turn off bold text too?

NO_COLOR should not turn off bold text, according to the convention's own FAQ, because the variable signals only a wish for no added color. Bold and underline codes can therefore still appear in output that a test expects to be plain.

How do you strip ANSI escape codes from a log file?

ANSI escape codes can be stripped from a log file with a regular expression that deletes each sequence from the escape byte to its final letter. Color and style codes end in the letter m. Cursor movement codes end in other letters and need a wider pattern.

What is the CLICOLOR convention?

The CLICOLOR convention is an older pair of variables in which CLICOLOR asks for color on a terminal and CLICOLOR_FORCE asks for color everywhere. Its own page marks it as deprecated and recommends NO_COLOR and FORCE_COLOR instead.