You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
With the default Encoding.UTF8, output starting with a BOM followed by a real U+FEFF loses both, so the same bytes decode differently per Encoding instance #145
AsyncProcessStreamReader builds its readers with detectEncodingFromByteOrderMarks: false (RunCommand/AsyncProcessStreamReader.cs:29-33) and then removes one leading U+FEFF itself in StripLeadingByteOrderMark (AsyncProcessStreamReader.cs:216-235). The remarks there assume that turning detection off "also turned off the stripping that came with it".
That's only true for encodings with no preamble. StreamReader skips the encoding's own preamble whenever encoding.Preamble.Length > 0, whatever detectEncodingFromByteOrderMarks is set to. OutputHandler defaults to Encoding.UTF8 (OutputHandler.cs:47), which has a preamble. So for the default handler:
StreamReader drops the BOM.
StripLeadingByteOrderMark then drops the next U+FEFF, which is real content.
The same double strip happens with Encoding.Unicode, Encoding.BigEndianUnicode and Encoding.UTF32.
Reproduction
A command writes the bytes EF BB BF EF BB BF 41 (a BOM, then U+FEFF, then A):
"A": only the BOM is gone, which is what the doc comment promises
The underlying StreamReader behaviour is easy to confirm on its own. With detectEncodingFromByteOrderMarks: false, new StreamReader(new MemoryStream(bytes), Encoding.UTF8, false).ReadToEnd() returns FEFF 41, while the same call with new UTF8Encoding(false) returns FEFF FEFF 41.
Why it matters
The impact is low, but the data loss is silent and it hits the default configuration. Take a BOM-emitting tool that echoes a file which itself starts with a BOM: its captured output loses a character. Output should also not depend on which of two equivalent UTF-8 Encoding instances the caller passed.
Suggested fix
Strip in exactly one place. Either:
only call the manual strip when outputHandler.Encoding.GetPreamble().Length == 0, or
build the readers from a preamble-free copy of the encoding (for example new UTF8Encoding(false, throwOnInvalid) with the caller's settings preserved, or a wrapper whose Preamble is empty) and keep the manual strip.
Acceptance criteria
A regression test feeds a doubled BOM through Encoding.UTF8 and through new UTF8Encoding(false), and both deliver "A".
The same test covers Encoding.Unicode, so the UTF-16 path is pinned too.
Priority: Low. The data loss is silent and happens in the default configuration (Encoding.UTF8), but it only shows up when output starts with a BOM followed by a real U+FEFF, which is rare.
Area / suggested assignment:RunCommand/AsyncProcessStreamReader.cs, where the readers are built (29-33) and in StripLeadingByteOrderMark (216-235). This is the output-decoding path.
Next step: Strip in one place only. Either skip the manual strip when the encoding has a preamble, or build the readers from a copy of the encoding with no preamble. Pin Encoding.UTF8, new UTF8Encoding(false) and Encoding.Unicode with the doubled-BOM test.
Activity
matt-edmondson commentedon Nov 8, 2026
Triage
Encoding.UTF8), but it only shows up when output starts with a BOM followed by a real U+FEFF, which is rare.RunCommand/AsyncProcessStreamReader.cs, where the readers are built (29-33) and inStripLeadingByteOrderMark(216-235). This is the output-decoding path.Encoding.UTF8,new UTF8Encoding(false)andEncoding.Unicodewith the doubled-BOM test.Generated by Claude Code