Cleaning Text

How to find bidi control characters on Mac

You can do this with Note My — here is how to scan a file you did not write for the invisible code points that reorder a line, find every one of them, and remove them safely.

You have been sent a file — a pull request, a snippet from a forum, a block of code pasted out of a chat — and you want to know whether the text on screen is the text the machine will run. Bidirectional control characters are why that is a real question. They occupy no width, they survive copy and paste, and they can make a line render in an order that has nothing to do with the order it is stored in. This guide covers what they are, how to scan a file for them in Note My, how to look at each one, and how to remove them without touching anything else.

Note

The scan runs entirely on your Mac. There is no account and nothing is uploaded, so a file you are inspecting precisely because you do not trust it never leaves the machine in order to be checked.

The code points that reorder text without changing it

Unicode has to render Arabic, Hebrew and Latin in one paragraph, so it carries characters whose only job is to steer the direction of the surrounding text. They are legitimate. They are also invisible, and in a source file they are almost always wrong.

Note My groups all twelve of them into one category, shown in the app as Bidi control characters:

The code points in the Bidi control characters category
Code pointsWhat they are
U+200E, U+200FLeft-to-right and right-to-left marks — direction hints with no glyph
U+202A–U+202EThe embeddings, the pop, and the two overrides. U+202E, right-to-left override, is the one that turns up in demonstrations of this trick
U+2066–U+2069The isolates and their pop
U+061CArabic letter mark

The effect that matters for code is simple. A compiler, an interpreter and a diff tool read the bytes in storage order; your editor, your terminal and your code-review page render them in display order, which these characters change. Insert one inside a comment or a string literal and the visible line can say one thing while the parsed line says another. Nothing about the file looks edited.

How seriously to take this finding

Note My sorts stray characters into categories, and every finding arrives with a severity you can read off the results list. Bidi controls sit at the top. Their severity is danger, the strongest of the three levels the app shows, and their default action is remove. They are safe to take out anywhere — any language, any part of a document — which is why the app is decisive about them where it is cautious about almost everything else.

That has two consequences worth knowing before you scan. The first is that the category is switched on in every one of the four presets — Safe, Code-safe (the default), Prose and Aggressive — so whichever one you pick, none of them quietly passes over a direction override.

The second is that bidi controls are among the few findings the app will take out for you in a single click, without opening a sheet and asking you to decide row by row. The practical reading: an orange pill is worth a look when you have a minute, and a red pill on a file you did not write is worth a look now, before you run it or merge it.

The status pill, and what "risky" means

A pill in the status bar reports what our hidden-character scanner has found. It has five states:

States of the hidden-character status pill
PillMeaning
HiddenIdle, waiting for its first look at the document. Switch on Check While Typing and it reports as you work.
"Scanning…"A scan is under way.
Clean, greenThe scan ran and came back with nothing to report.
"N hidden", orangeFindings, none of them danger-severity.
"N hidden · D risky", redFindings, of which D are danger-severity.

The red state is the one that matters here. "Risky" is the pill's word for danger severity, and a bidi control is danger by default. Click the pill and a popover opens offering Show in Text, Remove Invisible Character(s), Review All… and Go to Next, plus switches for Protect Strings & Comments and Check While Typing. Nothing is preselected, so the row that changes your document is never sitting armed under the Return key. The popover also carries a distinct not checked verdict, which exists so a document nobody has scanned is never presented as clean.

Scan a file you did not write

The pill is your running signal: it tells you when a file is worth a closer look. The Review All sheet is the full account of one. Every category is available to it, it works on a selection or on the whole document, and no document is too large for it.

Reach for it whenever the text arrived from somewhere you cannot vouch for: a pull request from a first-time contributor, a snippet lifted off a forum, a configuration file pasted into a support ticket.

  1. Open the file

    Press ⌘O, or drag the file onto the tab bar. If macOS insists the file is not text — no extension, or an extension owned by something else — use ⇧⌘O for File ▸ Open Anything as Text….

  2. Open the Review All sheet

    From the toolbar's Clean menu, choose Clean Invisible / Special Characters…. The Clean menu is the way in, and it holds the rest of the scanning commands too, so it is worth a look while you are there.

  3. Set the scope and the preset

    Scope is a segmented control: Selection or Whole Document. For a file you are vetting, use Whole Document. The preset pop-up offers the four presets plus Custom; every preset includes bidi controls, so the default Code-safe is fine. Ticking any category in the list flips the pop-up to Custom, which is expected rather than a warning.

  4. Press Scan

    The left-hand table lists the categories danger-first, each with a checkbox, a severity chip and a count, so anything serious appears without scrolling. Line and column numbers always refer to the whole document, even when you scanned a selection, so a Line:Col in the results matches what you see in the editor.

  5. Read the findings

    The right-hand table gives one row per finding: Line:Col · Code Point · Category · Action · Risk. The code-point column is the thing to look at — it tells you whether you are looking at U+202E or a stray U+200E, which is the difference between an attack and a paste artefact from a right-to-left document.

Look at each one in the text

Reading a table of line numbers is not the same as seeing where the characters sit. Three things bring you back to the document.

Double-click a finding in the sheet's results table and it is revealed in the editor. Show in Editor in the sheet's footer does the same for a selection of rows.

In-editor markers. Findings are marked in the document itself. A character that takes up no width of its own is given a visible marker, so a direction override becomes something you can see and click beside rather than a gap between two letters. The marker pulses when it first appears, then settles at a lower emphasis, so it stays findable without shouting at you while you work; with Reduce Motion switched on, it arrives at the settled state straight away. Markers are controlled by Highlight findings in Settings ▸ Text Cleanup, on by default.

Step through them. The Clean menu carries Find Next Hidden Character and Find Previous Hidden Character. Each selects the character, scrolls to it, shows a find indicator and wraps round at the end of the document, so you can walk a file from its first finding to its last and back round to where you started. Go to Next in the status-pill popover does the same job.

Warning

Reach for the right view. The toolbar's Show Invisibles toggle is the whitespace view: it draws spaces, tabs, the non-breaking-space ring and line-ending labels. Direction controls get a view of their own — the markers our hidden-character scanner leaves in the text, which give a character with no width something you can see and click beside.

Remove them, in one undo step

There are two routes, and both collapse into a single undoable action.

From the sheet, Apply (N) acts on every finding whose category is ticked and whose own action is destructive, as a single undoable action named Clean Invisible Characters; the results refresh so you can see what is left. From the status-pill popover, Remove Invisible Character(s) does the smaller version of the same thing, named for the number of characters it took out. Either way, ⌘Z once puts everything back.

Emoji and combining scripts are protected. A family emoji is not taken apart by a clean-up aimed at invisible junk, and a joining character doing real work inside a single visible character is reported for information only rather than removed.

One habit is worth forming before you press Apply on someone else's file. Note the Line:Col of anything at danger severity somewhere outside the file, because once you clean, the evidence is gone. And tell whoever sent it to you; they may not know it is still in the copy they are working from.

Why they go even inside strings and comments

There is a protection switch, Protect authored text, on by default — the same one the status-pill popover shows as Protect Strings & Comments. With it on, a finding that sits inside a string, a comment or ordinary prose is listed but left alone: reported, never acted on. It is why a curly quote inside a comment survives a clean.

Two things about that switch are worth knowing, and both work in your favour here. The first is that the Review All sheet lays out the complete list and lets you act on any of it: the Action column tells you what Apply would do to each row, and the ticked categories decide which rows are in scope. That keeps the decision with you. If a row looks alarming, check the Line:Col, go and look at the character in the text, and untick any category you would rather leave exactly as the author wrote it.

The second is the one that matters for direction controls: the protection never applies to them. Bidi controls are safe to remove anywhere, so they are reported and removable wherever they turn up, inside a string literal most of all. A direction override in a string misleads a reader as well as one in open code; arguably better, since a string is where you would hide one. That holds whatever the file is. Our advanced language identifier keeps the protection aimed at the right thing for the text you are working in, so you never have to tell the app what it is looking at — and a direction control stays reported, and stays removable, in every kind of file: a source file in a language you use daily, a configuration file, a plain block of prose pasted out of a chat.

Tune the pill to the files you work in

The pill is built for speed: it watches while you type and flags the obvious problem the moment it appears, so a suspect paste announces itself before you have finished the line. How much of a document it watches is yours to set. Raise the Scan documents up to slider in Settings ▸ Text Cleanup and it keeps reporting on progressively bigger files; lower it and it stays out of the way on the long ones. When you want the complete account of a file rather than a running signal, open the sheet — that is the job it was built for.

The pill is a status-bar item you control, listed as "Hidden characters" at Settings ▸ General ▸ Status Bar. Leave it on to keep the readout in front of you as you work, or switch it off for a quieter status bar and run the sheet whenever you want an answer. Whichever you choose, the sheet stays one item away in the Clean menu, so the thorough answer is always within reach of the file in front of you.

The Review All sheet has no size limit of its own. Our large-file engine keeps a very large document open and usable, so the sheet gives you a complete answer on one: start it, leave it, come back to the results — all of it on your Mac, however large the file.

Troubleshooting

The pill says Clean but I still do not trust the file

A green pill is the quick check coming back clear, which is a good sign. When you want the fuller picture on top of it, run Clean ▸ Clean Invisible / Special Characters… with the scope set to Whole Document. The sheet reports against the categories the chosen preset enables, and Aggressive turns every category on, so that is the preset to pick when you want everything the app can name laid out in front of you.

Bring the pill back

Three things to check, in order of likelihood: the scan found nothing and the pill is sitting idle; the document is bigger than the Scan documents up to setting in Settings ▸ Text Cleanup, which you can raise to cover it; or the status-bar item is switched off in Settings ▸ General ▸ Status Bar, where a single toggle puts it back. The sheet gives you your answer in all three cases.

Find Next Hidden Character just beeps

It beeps when there is nothing to move to: no findings in the document, or none left to wrap round to. Scan again after an edit and it has a fresh set to walk you through. On a very large file it will think for a moment before it jumps, and our large-file engine keeps the document responsive while it does.

The markers vanish the moment I type

That is deliberate. As soon as you start editing, the marks would no longer sit on the characters they were put there for, so they clear rather than point you at the wrong place. Scan again when the edit is finished and they come back, matching the document as it now stands.

Apply is greyed out

Apply lights up, with its count, as soon as one finding is both in a ticked category and carries a removable action of its own. A document whose only findings are report-only — homoglyphs, combining marks — is being shown to you for information, which is the app leaving that judgement where it belongs, with you. Tick a category that holds a removable finding and Apply is ready.

What else you can do with a file you did not write

  • Survey the whole project first. Search ▸ Find in Folder… (⇧⌘F) searches a folder and everything inside it, with optional regular expressions, showing matches as they turn up and grouping them by file. Double-click a result and the file opens at that line, ready to scan and clean in its own tab. The search only ever reads, so you can sweep an unfamiliar repository knowing it comes out exactly as it went in.
  • Keep the call yours. The sheet gives you the code point and the exact position, and every row is one double-click away from the character sitting in the text. Whether a mark is an attack, a paste artefact from a right-to-left document or something the file genuinely needs, you make that judgement with the evidence in front of you rather than from a table alone.
  • Clear AI paste residue in one click. Clean AI Copy/Paste Markers in the Clean menu handles the artefacts that ride along with text pasted out of an AI chat, and it holds itself to the ones that cannot change visible rendering, wording or punctuation. Anything needing judgement is counted and reported so you can look at it properly in the sheet.
  • Write your own removal rules. Clean ▸ Edit Removal Rules… arrives with rules for zero-width characters, the word joiner, soft hyphens and line separators, and you can add your own with an empty replacement to strip whatever pattern your files keep throwing at you. Bidi controls are handled by the scan itself, so the rule list stays free for everything else.
  • Work a long file end to end. Find Next Hidden Character and Find Previous Hidden Character in the Clean menu walk you through the findings in order, selecting each one, scrolling to it and wrapping round at the end of the document, so a thousand-line file can be reviewed without losing your place.
  • Set the presets to your work. Safe, Code-safe, Prose and Aggressive cover most of what you will meet, and ticking any category yourself makes a Custom set on the spot. Every one of them includes bidi controls, so the choice is about what else you want to see alongside them.

Frequently asked questions

What is U+202E and why does it matter in source code?

It is the right-to-left override: it forces the characters after it to be laid out in the opposite direction. Because rendering changes and storage does not, a line containing one can display in an order that differs from the order a compiler or interpreter reads. Note My reports it as part of the Bidi control characters category, at danger severity.

Will removing these break emoji or non-Latin text?

Emoji are protected: a multi-character emoji is kept whole, and a joining character doing real work inside one is reported for information only rather than removed. Genuine right-to-left prose is a different matter. If you are editing a document that legitimately mixes scripts, review the findings individually rather than pressing Apply: some of those direction marks may be there on purpose.

How do I see a direction control in the text?

Our hidden-character scanner marks each finding in the document itself, so a character with no width of its own becomes something you can see and click beside. It is on by default, as "Highlight findings" in Settings ▸ Text Cleanup. Double-clicking a row in the sheet reveals that character in the editor, and Find Next Hidden Character walks you from one to the next. Show Invisibles is the separate whitespace view: spaces, tabs, a ring for a non-breaking space, and LF, CRLF or CR labels.

Can I check a whole folder at once?

You can search one, which is the quickest way to find the files worth opening. Find in Folder (⇧⌘F) searches a folder and everything inside it, with optional regular expressions, and groups the matches by file; double-clicking a result opens that file at that line, where the scan and the clean happen in the tab. The search itself never writes to the files it reads, so a sweep through an unfamiliar project leaves every one of them as you found it.

Is one undo enough after cleaning?

Yes. Apply in the sheet is a single undoable action named "Clean Invisible Characters", and the popover's removal is a single action named for the number of characters it took out. One ⌘Z reverses the whole thing.

Does the file get uploaded anywhere to be scanned?

It all happens on your Mac. There is no account, nothing to sign in to, and nothing is uploaded. Every scan described here runs on the text in front of you, and the file never leaves the machine — which is the point, since the files worth scanning are the ones you were sent by somebody else.