check-unicode
A pre-commit hook to detect and fix non-ASCII Unicode characters in text files.
Catches smart quotes, em dashes, fancy spaces, dangerous invisible characters (Trojan Source bidi attacks, zero-width chars), and other copy-paste artifacts.
Installation
As a pre-commit hook
repos:
- repo: https://github.com/mit-d/check-unicode
rev: v0.6.0
hooks:
- id: check-unicode
# or for auto-fix:
- id: fix-unicode
Standalone
pip install check-unicode
check-unicode path/to/file.txt
Usage
check-unicode [OPTIONS] [FILES...]
| Flag | Description | Default |
|---|---|---|
--fix |
Replace known offenders with ASCII, exit 1 if changed | off |
--allow-range RANGE |
Allow a Unicode range (e.g. U+00A0-U+00FF). Repeat. |
none |
--allow-codepoint CP |
Allow codepoints (e.g. U+00B0). Repeat/comma-sep. |
none |
--allow-category CAT |
Allow Unicode category (e.g. Sc). Repeatable. |
none |
--allow-printable |
Allow all printable chars (only flag invisibles) | off |
--allow-script SCRIPT |
Allow Unicode script (e.g. Latin). Repeatable. |
none |
--check-confusables |
Detect mixed-script homoglyph/confusable characters | off |
--severity LEVEL |
error (exit 1) or warning (print, exit 0) |
error |
--no-color |
Disable ANSI color | auto-detect |
--config FILE |
Path to TOML config | auto-discover |
-q / --quiet |
Summary only | off |
-V / --version |
Print version |
What it catches
- Smart quotes:
\u201c\u201d\u2018\u2019and variants - Dashes: em dash
\u2014, en dash\u2013, minus sign\u2212 - Fancy spaces: non-breaking space, em space, thin space, etc.
- Ellipsis:
\u2026 - Dangerous invisible characters (always flagged):
- Bidi control (Trojan Source CVE-2021-42574): U+202A-202E, U+2066-2069
- Zero-width: U+200B-200F, U+FEFF (mid-file), U+2060-2064, U+180E
- Replacement character: U+FFFD
- Confusable homoglyphs (with
--check-confusables):- Mixed-script identifiers where minority-script chars look like Latin
- Cyrillic/Greek/Armenian letters that visually resemble Latin letters
- e.g. Cyrillic
a(U+0430) mixed with Latinccess_level
Auto-fix
--fix replaces known offenders with ASCII equivalents:
| Unicode | Replacement |
|---|---|
| Smart quotes | ' or " |
| En/em dashes | -- |
| Minus sign | - |
| Fancy spaces | |
| Ellipsis | ... |
Dangerous invisible characters are never auto-fixed -- they require manual review.
Configuration
Create .check-unicode.toml or add to pyproject.toml:
[tool.check-unicode]
allow-codepoints = ["U+00B0", "U+2192"]
allow-ranges = ["U+00A0-U+00FF"]
allow-categories = ["Sc"]
allow-printable = true
allow-scripts = ["Latin", "Cyrillic"]
check-confusables = true
severity = "error"
Per-file overrides
Use [[tool.check-unicode.overrides]] to apply different settings per file
pattern. Each entry requires a files key with a list of glob patterns and can
set any of the options above.
Allow-lists (allow-codepoints, allow-ranges, allow-categories,
allow-printable, allow-scripts) are additive -- they merge with the
global config. severity and check-confusables replace the global value
for matching files.
# Relax rules for documentation
[[tool.check-unicode.overrides]]
files = ["docs/*.md", "*.rst"]
allow-printable = true
severity = "warning"
# Allow non-Latin scripts in i18n files
[[tool.check-unicode.overrides]]
files = ["i18n/**"]
allow-scripts = ["Cyrillic", "Han"]
check-confusables = false
Output
path/to/file.txt:42:17: U+201C LEFT DOUBLE QUOTATION MARK [Ps]
He said \u201chello\u201d to the crowd
^
Found 5 non-ASCII characters in 2 files (3 fixable, 1 dangerous)
Development
uv venv && uv sync --group dev
.venv/bin/pytest -v --cov
.venv/bin/ruff check src/ tests/
uvx ty check src/
License
MIT
Metadata
Release files for check-unicode 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| check_unicode-0.6.0.tar.gz | 26.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| check_unicode-0.6.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 51.4 kB
Release files / check_unicode-0.6.0.tar.gz
| Download URL | check_unicode-0.6.0.tar.gz |
|---|---|
| Size | 26.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
33ec600d93760309d1d15259e090346b4e5f3e2c8d4b3f63f7f94d65ec8530f3
|
|
BLAKE2b-256 checksum How to use checksums |
2bde6a40967aa126f790785cce1f8a1435b384b701a2d85649933fed5f85d266
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 29, 2026.
Transparency logRelease files / check_unicode-0.6.0-py3-none-any.whl
| Download URL | check_unicode-0.6.0-py3-none-any.whl |
|---|---|
| Size | 25.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b81e7bd6d8b8716ebe9a23707c82ce7e95ee1411f4c35a5af921aa192d6d6b19
|
|
BLAKE2b-256 checksum How to use checksums |
1b32d7c9e3a03f74f14e35e5cd3bb40c083b9a937b2f31182065cc32cc5e14b6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 29, 2026.
Transparency log