Console color without wrecking your log files

DetentShell · August 2026 · 2 min read

PowerShell 7 colors its output with ANSI escape codes. Get-ChildItem gets colored file names, table headers come out green, and in a terminal the engine is careful about where those codes go. Text headed for the screen keeps them. Text you capture with Out-String or redirect with > gets them stripped. You get a colorful console and clean log files at the same time because the engine knows the destination.

Host that engine inside a GUI app and the arrangement falls apart. Our first implementation forced color codes on for everything so our console could render them. The console looked right. The side effect was ugly: the same setting applied to code running inside the app, so a script that did $text = $data | Format-List | Out-String got escape codes in the string, and Get-ChildItem > files.txt wrote escape codes into the file. The identical script in Windows Terminal produced clean output. Our engine-parity tests failed on exactly this, and a person running our manual checklist hit it too, as a failed check in the middle of an otherwise green run.

We measured two cheap fixes before rejecting them. Stripping codes everywhere restores correct captures and kills console color entirely. Intercepting Out-String and Out-File with session shims fixes those two commands but can’t reach >, because redirection binds the cmdlet at compile time, below anything a session can override. Half a fix for this bug means a user still finds escape codes in a log file eventually.

The real fix was to make the app behave like what PowerShell already understands: a terminal. Engine output now renders through the app’s own PowerShell host, and that host declares support for virtual terminal sequences. With that in place the engine’s stock behavior does everything. Screen-bound text arrives at our renderer with color intact. Captured and redirected text is stripped by the engine itself, the same way it is in pwsh. Your scripts see nothing unusual because nothing unusual is happening to them.

The rework fixed a small oddity we hadn’t prioritized: output piped explicitly to Out-Host used to vanish, since nothing was listening on the host side. Now the host is the output path, so Out-Host shows up like it should.

On our side of the glass, the renderer turns the escape sequences into colored text runs, drops anything unrenderable, and strips codes when you copy text out, so the clipboard gets plain text.

Five tests now pin the arrangement: in-session captures come back clean, redirected files come back clean, color reaches the renderer, the session’s rendering setting is never touched, and Out-Host output is visible. Any of them flipping fails the build.

← All posts