MarkCleaner / Field notes
ChatGPT Hidden Characters: Find Where They Entered Your Text
A character scan can tell you what is in a string. To find where it appeared, save a local snapshot at every handoff and compare each pair in order.
Trace the handoffs, not just the final text
If you inspect only the final passage, a scanner cannot show whether a character was in the original draft or entered later. A useful troubleshooting record is a short chain of snapshots. Give every snapshot a stage name, keep the text local, and compare the output of one stage with the input of the next.
| Snapshot | What to save | Question it answers |
|---|---|---|
| 1. Draft | The exact text before leaving the source chat | Was the code point already present in this copy? |
| 2. Editor | The text after pasting into the writing app | Did the paste or editor change the character counts? |
| 3. CMS | The text copied back from the CMS field or preview | Did the CMS input, formatter, or save step change the string? |
| 4. Published page | The text copied from the rendered page | Did publication or rendered content differ from the saved CMS version? |
This is a synthetic example. The non-breaking space is deliberately written as an escape sequence so the sample itself is easy to inspect:
const stages = [
["Draft", "A B"],
["Editor", "A\u00A0B"],
["CMS", "A\u00A0B"],
["Published copy", "A B"]
];
The first comparison reports a swap from U+0020 SPACE to U+00A0 NO-BREAK SPACE at the editor handoff. The CMS snapshot matches the editor snapshot. The published copy changes back to an ordinary space. This example shows how the method reads; it is not a measurement of any real app.
Compare each adjacent pair locally
The short script below counts Unicode code points in each snapshot and reports count changes for the next stage. Paste only text you are allowed to inspect. It makes no network requests and does not modify the snapshots.
function codePointCounts(text) {
const counts = new Map();
for (const character of text) {
const point = "U+" + character.codePointAt(0)
.toString(16).toUpperCase().padStart(4, "0");
counts.set(point, (counts.get(point) || 0) + 1);
}
return counts;
}
for (let i = 1; i < stages.length; i++) {
const before = codePointCounts(stages[i - 1][1]);
const after = codePointCounts(stages[i][1]);
const points = new Set([...before.keys(), ...after.keys()]);
const changes = [...points]
.map(point => ({
point,
before: before.get(point) || 0,
after: after.get(point) || 0
}))
.filter(row => row.before !== row.after);
console.log(stages[i][0], changes);
}
JavaScript for...of string iteration yields code points, keeping a supplementary character such as many emoji together as one code point. A code point is not always one user-perceived character: joined emoji and combining sequences can contain several code points. This count report also misses a reorder when each code point's total stays the same. If order matters, compare the full snapshots as well.
Read the result as a stage clue
- A difference at the editor snapshot: compare the saved draft directly with the text copied back from the editor. Repeat once with a plain-text field to narrow the handoff.
- A difference only at the CMS snapshot: compare what you pasted with what the CMS returns after saving. Record whether you read the editable field, preview, or published DOM.
- A difference only on the published page: check page text and rendered text separately. A line wrap can be CSS behavior even when the underlying text is unchanged.
- No count difference: code-point totals match. That does not establish identical order or formatting; compare the full strings and inspect the rendered page.
Keep the original and each stage copy until you understand the change. If cleanup is needed, make a separate output and check it in the final destination. That preserves a before-and-after record instead of erasing the clue.
Sources and scope
- Unicode Standard, Chapter 23 describes layout roles such as NO-BREAK SPACE and WORD JOINER.
- MDN: String.prototype[Symbol.iterator]() explains code-point iteration and why code points differ from grapheme clusters.
- OpenAI's content-provenance research update describes text watermarking as research; it does not identify a particular Unicode character in a pasted passage as an OpenAI watermark.
This guide is a local troubleshooting method for comparing copies you control. The code and sample changes are illustrative, not user data or product test results.