admins and аdmins are two different variables. Here's how to catch lookalike names before they ship.
SummaryOne Cyrillic letter can give a Python variable a twin that looks the same in a diff. Python runs it without a word, and Ruff's default rules pass it. We tested which checks catch it and which don't.
· MODEL Editors · 3 min read
Some links may be affiliate links. Disclosure

The Unicode Consortium's security report, UTS #39, was on the Hacker News front page today. It's the document behind "confusables": characters from different scripts that look the same, like the Latin a and the Cyrillic а (U+0430). The report covers how to detect them. Most of the attention goes to lookalike domain names, but the same trick works in source code. More code now arrives from agents and drive-by pull requests, and nobody reads it letter by letter. So we checked which tools notice a lookalike name.
The test. We wrote a five-line Python function:
def can_delete(user):
admins = {"alice"}
аdmins = admins | {"mallory"}
return user in аdmins
The third and fourth lines use a Cyrillic а. In a terminal, an editor or a diff, it reads like a harmless line that the return statement ignores. Python treats аdmins as a separate variable, so the function checks the list with mallory in it. python3 bypass.py printed mallory can_delete: True. Python 3.13.5 gave no warning.
Python also merges some names you'd think were different. The Python docs say all names are converted to the normal form NFKC while parsing. So a fullwidth i (U+FF49) folds to a plain i. In our second file, is_admin = True silently overwrote is_admin = False, and the script printed True. Cyrillic letters don't fold, so аdmins stays separate. Either way, the code doesn't do what it looks like it does.
What caught it:
- Ruff 0.17.2 with its default rules: nothing.
ruff checkprinted "All checks passed!" on both files. - Ruff with two opt-in rules: caught.
ruff check --select PLC2401,PLC2403,RUF001,RUF002,RUF003flaggedаdminsandis_adminas PLC2401 ("Variable name contains a non-ASCII character") and exited 1. It took 0.02 seconds on our two files. RUF001 caught a Cyrillicаinside a string and named it: "Did you meana(LATIN SMALL LETTER A)?" PLC2403 covers import names, and RUF002 and RUF003 cover docstrings and comments. - Rust: caught by default. We ported the same trick to Rust.
rustc1.85.1 with no flags warned "found bothadminsandаdminsas identifiers, which look alike" (lintconfusable_idents). It added amixed_script_confusableswarning naming U+0430. Both lints are on by default. - A plain grep works for any language.
grep -nP '[^\x00-\x7F]'printed both lines. To check only what you're about to commit, rungit diff --cached -U0 | grep -P '^\+(?!\+\+).*[^\x00-\x7F]'. It flagged the two added lines in our test repo.
The tip. For Python, put the rules in your config so they run everywhere, including CI and your agent's own lint step:
[tool.ruff.lint]
extend-select = ["PLC2401", "PLC2403", "RUF001", "RUF002", "RUF003"]
With that in pyproject.toml, a plain ruff check flagged the committed file and exited 1.
The catches.
- PLC2401 is a blunt rule. It flags every non-ASCII name, not just lookalikes. It flagged
café = 1too. If your team uses non-English names on purpose, expect noise or add per-file ignores. - Rust only warns. The program still compiled, ran and printed
true. Add#![deny(confusable_idents, mixed_script_confusables)], or fail CI on warnings, if you want it to block a merge. - The grep catches every non-ASCII character, including emoji and accented text in strings. It's a review prompt, not a verdict.
- We didn't test JavaScript, TypeScript or Go tooling, editors' own Unicode highlighting, or GitHub's diff view.
Our take
Two minutes of config. If agents or outside contributors write Python for you, turn on those five Ruff rules today. In Rust, make the two lints fail the build. Then a reviewer can trust that a name that looks the same is the same.
What we did: we ran everything above on a Linux box on October 25, 2026, with Python 3.13.5, Ruff 0.17.2 (via uvx), rustc 1.85.1 and git 2.47.3, on small test files we wrote. Facts come from UTS #39, the Python language reference, PEP 672, Ruff's rule docs and the rustc lint listing, all read today. Not sponsored; no affiliate links.
MODEL Editors
Sources
- UTS #39: Unicode Security Mechanisms (version 18.0.0)
Unicode Consortium
- UTS #39: Unicode Security Mechanisms (Hacker News discussion)
Hacker News
- Lexical analysis: Names (identifiers and keywords)
Python documentation
- PEP 672: Unicode-related Security Considerations for Python
Python Software Foundation
- non-ascii-name (PLC2401)
Ruff documentation
- ambiguous-unicode-character-string (RUF001)
Ruff documentation
- Ruff 0.17.2 release
astral-sh/ruff on GitHub
- Warn-by-default lints: confusable_idents, mixed_script_confusables
The rustc book
FREE · EVERY WEEK
The top AI tools this week, tested.
Every week MODEL's editors pick the AI tools worth your time, try them hands-on, and tell you what's good, what's not, and who it's for. Free, in your inbox.
Free. Unsubscribe anytime. Our picks are editorial and can't be bought.
The weekly three is editorial. A subscription does not buy a place in it.
Continue

The test
TypeScript 7.1 beta, tested: it catches the import you forgot to label, and it moved the API
The first feature release of the Go-based TypeScript compiler adds a stable API for tools and type rules for import attributes. We ran it on our own site and on small test projects next to 7.0.2 and 5.9.3. The fixes hold. If your tooling imported the 7.0 API, it breaks.
· MODEL Editors

Builder tip
Patched and unpatched Node both say brotli 1.1.0. Here's how to check for Node2Shell.
The Brotli decoder flaw known as Node2Shell (CVE-2026-111497) sits inside Node.js, the PyPI Brotli packages and plenty of HTTP clients. Your app doesn't have to import Brotli to be exposed. We checked Node and Python on a Linux box and found the version numbers you'd look at first can mislead you.
· MODEL Editors

The test
agent-browser 0.39.2, tested: it stopped telling your agent that dead clicks worked
Vercel Labs' browser tool for AI agents shipped a fix this week for clicks that reported success without ever reaching the page. We ran the old and new versions side by side on our own test pages, with no AI model and no API key. The fix holds. The catch is in the docs' own security example.
· MODEL Editors