<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: brkn</title>
    <description>The latest articles on DEV Community by brkn (@brknbrkn).</description>
    <link>https://dev.to/brknbrkn</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4190194%2Fc1350290-29d0-4ae5-909f-8e6da4d857b7.png</url>
      <title>DEV Community: brkn</title>
      <link>https://dev.to/brknbrkn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/brknbrkn"/>
    <language>en</language>
    <item>
      <title>Why Your JSON Fails to Parse: Zero-Width Spaces Copied from ChatGPT</title>
      <dc:creator>brkn</dc:creator>
      <pubDate>Sat, 17 Oct 2026 09:28:26 +0000</pubDate>
      <link>https://dev.to/brknbrkn/why-your-json-fails-to-parse-zero-width-spaces-copied-from-chatgpt-4p8k</link>
      <guid>https://dev.to/brknbrkn/why-your-json-fails-to-parse-zero-width-spaces-copied-from-chatgpt-4p8k</guid>
      <description>&lt;p&gt;You ask ChatGPT for a config snippet. You copy it, paste it into your editor, and compare it with the file that works. The two look identical, character for character. Then you run it.&lt;/p&gt;

&lt;p&gt;In Node, a pasted file that starts with a byte order mark gives you this (the invisible character is written here as &lt;code&gt;&amp;lt;U+FEFF&amp;gt;&lt;/code&gt;; your terminal prints nothing at that position):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SyntaxError: Unexpected token '&amp;lt;U+FEFF&amp;gt;', "&amp;lt;U+FEFF&amp;gt;{"name": "x"}" is not valid JSON
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Python, a zero-width space pasted into source code gives you this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SyntaxError: invalid non-printable character U+200B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The BOM variant of the Python error is the same message with &lt;code&gt;U+FEFF&lt;/code&gt; at the end. In both languages the message names a character you cannot see, and the line it points to looks fine. The string you pasted is not the string you think it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is actually in the string
&lt;/h2&gt;

&lt;p&gt;These are the code points that cause most of the trouble:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code point&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Where it usually comes from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;U+200B&lt;/td&gt;
&lt;td&gt;Zero width space&lt;/td&gt;
&lt;td&gt;Rendered web text, line-break hints, chat UIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U+200C&lt;/td&gt;
&lt;td&gt;Zero width non-joiner&lt;/td&gt;
&lt;td&gt;Persian, Arabic and Indic text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U+200D&lt;/td&gt;
&lt;td&gt;Zero width joiner&lt;/td&gt;
&lt;td&gt;Emoji sequences, Indic scripts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U+2060&lt;/td&gt;
&lt;td&gt;Word joiner&lt;/td&gt;
&lt;td&gt;Typesetting, some rich-text editors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U+FEFF&lt;/td&gt;
&lt;td&gt;Byte order mark / zero width no-break space&lt;/td&gt;
&lt;td&gt;"UTF-8 with BOM" files, Windows tools, CSV exports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U+00A0&lt;/td&gt;
&lt;td&gt;No-break space&lt;/td&gt;
&lt;td&gt;HTML &lt;code&gt;&amp;amp;nbsp;&lt;/code&gt;, PDFs, rich-text copy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U+200E / U+200F&lt;/td&gt;
&lt;td&gt;Left-to-right / right-to-left mark&lt;/td&gt;
&lt;td&gt;Mixed-direction text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U+00AD&lt;/td&gt;
&lt;td&gt;Soft hyphen&lt;/td&gt;
&lt;td&gt;Hyphenated web pages and PDFs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why do AI chat interfaces leave them behind? The usual explanation is that the chat UI builds its output incrementally and renders it as rich text, and some of that structure ends up in the clipboard when you copy. These are rendering and streaming artifacts, not watermarks. Nothing about them is a hidden signal, and they are not tied to the content of the answer. The same characters show up when you copy from PDFs, Slack, Notion and ordinary web pages.&lt;/p&gt;

&lt;p&gt;One more thing worth knowing: a zero-width space inside a JSON string is valid JSON. &lt;code&gt;JSON.parse&lt;/code&gt; accepts it. The failure is later, when &lt;code&gt;config["name"]&lt;/code&gt; returns &lt;code&gt;undefined&lt;/code&gt; because the key is really &lt;code&gt;"name\u200B"&lt;/code&gt;. A parse error only happens when the character lands outside a string, or when it is a BOM at the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to see them
&lt;/h2&gt;

&lt;p&gt;Shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; file.json        &lt;span class="c"&gt;# BOM shows as M-oM-;M-?, U+200B as M-bM-^@M-^K&lt;/span&gt;
xxd file.json | &lt;span class="nb"&gt;head&lt;/span&gt;    &lt;span class="c"&gt;# a BOM is the bytes ef bb bf at offset 0&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nP&lt;/span&gt; &lt;span class="s1"&gt;'[\x{200B}\x{FEFF}\x{00A0}]'&lt;/span&gt; file   &lt;span class="c"&gt;# needs a UTF-8 locale&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;grep -P&lt;/code&gt; complains that a code point is too large, run it with &lt;code&gt;LC_ALL=C.UTF-8&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;repr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# repr escapes U+200B as \u200b
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mh"&gt;0x7e&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;codePointAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mh"&gt;0x7e&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Editor and git:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VS Code: &lt;code&gt;editor.renderControlCharacters&lt;/code&gt; draws control characters. For invisible Unicode, check the &lt;code&gt;editor.unicodeHighlight.*&lt;/code&gt; settings. The "Highlight Bad Chars" and "Gremlins" extensions also mark them.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git diff&lt;/code&gt; shows a changed line where the old and new text look the same. Run &lt;code&gt;git diff | cat -A&lt;/code&gt; and the difference appears as &lt;code&gt;M-&lt;/code&gt; sequences.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Strip the characters, but handle two cases separately. NBSP should become a normal space, not disappear, or &lt;code&gt;a\u00A0b&lt;/code&gt; turns into &lt;code&gt;ab&lt;/code&gt;. And U+200D must survive when it joins emoji, because that is how a family emoji is built: person, ZWJ, person, ZWJ, child. Remove every ZWJ and the family falls apart into three separate emoji.&lt;/p&gt;

&lt;p&gt;JavaScript, preserving ZWJ between emoji:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KEEP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;uE000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// private-use placeholder, not present in normal text&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cleanInvisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;
    &lt;span class="c1"&gt;// protect a ZWJ that sits between two emoji&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(\p&lt;/span&gt;&lt;span class="sr"&gt;{Extended_Pictographic}&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;FE0F&lt;/span&gt;&lt;span class="se"&gt;?)\u&lt;/span&gt;&lt;span class="sr"&gt;200D&lt;/span&gt;&lt;span class="se"&gt;(?=\p&lt;/span&gt;&lt;span class="sr"&gt;{Extended_Pictographic}&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/gu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`$1&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;KEEP&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// remove zero-width chars, direction marks, word joiner, BOM, soft hyphen&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;200B-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;200F&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;2060&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;FEFF&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;00AD&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// NBSP becomes a regular space&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;00A0/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// restore the protected joiners&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;KEEP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u200D&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;family&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u{1F468}&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u200D&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u{1F469}&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u200D&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u{1F467}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cleanInvisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;family&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;family&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cleanInvisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u200Bb&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u00A0c&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;uFEFF&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;    &lt;span class="c1"&gt;// "ab c"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python's &lt;code&gt;re&lt;/code&gt; has no &lt;code&gt;\p{...}&lt;/code&gt;, so this version approximates "emoji" with two common ranges. Adjust them to your data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="n"&gt;EMOJI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[\U0001F300-\U0001FAFF\u2600-\u27BF]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;KEEP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\uE000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;clean_invisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;rf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;EMOJI&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;\uFE0F?)\u200D(?=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;EMOJI&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;KEEP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="se"&gt;\u200B&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\u200F\u2060\uFEFF\u00AD&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\u00A0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KEEP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\u200D&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shell, using perl for the emoji-aware version and sed for the blunt one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# perl: keeps ZWJ between emoji, NBSP -&amp;gt; space&lt;/span&gt;
perl &lt;span class="nt"&gt;-CSD&lt;/span&gt; &lt;span class="nt"&gt;-pe&lt;/span&gt; &lt;span class="s1"&gt;'
  s/(\p{Extended_Pictographic}\x{FE0F}?)\x{200D}(?=\p{Extended_Pictographic})/$1\x{E000}/g;
  s/[\x{200B}-\x{200F}\x{2060}\x{FEFF}\x{00AD}]//g;
  s/\x{00A0}/ /g;
  s/\x{E000}/\x{200D}/g;
'&lt;/span&gt; file.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; file.clean.json

&lt;span class="c"&gt;# sed (GNU): byte-level, removes every ZWJ, so use it on code and config, not prose with emoji&lt;/span&gt;
&lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;C &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'s/\xEF\xBB\xBF|\xE2\x80[\x8B-\x8F]|\xE2\x81\xA0|\xC2\xAD//g; s/\xC2\xA0/ /g'&lt;/span&gt; file.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where it bites hardest
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JSON keys.&lt;/strong&gt; The file parses, but &lt;code&gt;obj.name&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt; because the key ends in U+200B.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.env files.&lt;/strong&gt; A trailing U+200B on a value gives you an API key that is one character too long, and the provider answers with an auth error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shell commands.&lt;/strong&gt; &lt;code&gt;cd my\u2011dir&lt;/code&gt;, where the hyphen is a non-breaking hyphen (U+2011), fails with "No such file or directory" even though the name looks right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;YAML.&lt;/strong&gt; Indentation made of NBSP is not indentation, so the parser reports a structure error on a line that looks aligned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL.&lt;/strong&gt; &lt;code&gt;WHERE name = 'Ada'&lt;/code&gt; returns no rows when the stored value has a trailing U+200B or an NBSP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL slugs.&lt;/strong&gt; A hidden character gets percent-encoded (U+200B becomes &lt;code&gt;%E2%80%8B&lt;/code&gt;), so the link is a different URL from the one you meant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSV headers.&lt;/strong&gt; A BOM glues itself to the first header, so &lt;code&gt;row["id"]&lt;/code&gt; fails and the column is really named &lt;code&gt;"\uFEFFid"&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prevent it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Paste with Ctrl/Cmd+Shift+V in editors that support paste-as-plain-text. This skips the rich-text clipboard layer, though characters already present in the plain-text layer still come through.&lt;/li&gt;
&lt;li&gt;Add a pre-commit hook. This one skips U+200D so legitimate emoji sequences do not block a commit, and it reads the working tree, not the staged blobs:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# .git/hooks/pre-commit&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;C.UTF-8
&lt;span class="k"&gt;if &lt;/span&gt;git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="nt"&gt;--diff-filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ACM &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | xargs &lt;span class="nt"&gt;-r0&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-InP&lt;/span&gt; &lt;span class="s1"&gt;'[\x{200B}\x{200C}\x{200E}\x{200F}\x{2060}\x{FEFF}\x{00AD}]'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Hidden Unicode characters found, commit aborted."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;ESLint's &lt;code&gt;no-irregular-whitespace&lt;/code&gt; covers U+00A0, U+FEFF and U+200B, but not U+200C, U+200D, U+200E, U+200F, U+2060 or U+00AD. It also ignores string literals by default (&lt;code&gt;skipStrings&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;), and strings are where pasted keys and values live.&lt;/li&gt;
&lt;li&gt;For a one-off snippet, &lt;a href="https://gpt-cleanup.com/zero-width-space-remover" rel="noopener noreferrer"&gt;gpt-cleanup.com/zero-width-space-remover&lt;/a&gt; lists every hidden character with its code point before it cleans anything, and there is a Chrome/Edge extension for right-click cleaning. Everything runs in the browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Copied text can carry invisible characters (U+200B, U+FEFF, U+00A0 and others) that look like nothing and break parsers.&lt;/li&gt;
&lt;li&gt;They are rendering and streaming artifacts, not watermarks.&lt;/li&gt;
&lt;li&gt;Find them with &lt;code&gt;cat -A&lt;/code&gt;, &lt;code&gt;xxd&lt;/code&gt;, &lt;code&gt;grep -P&lt;/code&gt;, or &lt;code&gt;repr()&lt;/code&gt; and code-point listings in Python and Node.&lt;/li&gt;
&lt;li&gt;Strip them with a regex, turn NBSP into a space, and keep ZWJ between emoji.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>python</category>
      <category>debugging</category>
      <category>unicode</category>
    </item>
  </channel>
</rss>
