hexsweep
Find the hardcoded colors that should be design tokens. After a token
migration, components still have raw background: #3f82f0 buried in them instead
of var(--primary-blue) — and you don't find out until dark mode or a rebrand
breaks. hexsweep scans your source and flags every raw color literal that
escaped, with zero config and zero dependencies.
pipx run hexsweep src/
# src/components/Button.tsx (2)
# 14:18 #3f82f0 [hex6] background
# 27:10 rgb(255, 0, 0) [rgb] color
#
# ✖ 2 hardcoded colors in 1 file (23 files clean)
Exits non-zero when it finds colors, so it drops straight into CI. Pure standard
library. Also on npm (npx hexsweep) — the two builds produce byte-for-byte
identical output.
Why not grep or stylelint?
grep '#[0-9a-f]{6}' is what people fall back to, and it's noisy: it hits
#header id-selectors, url(#gradient) references, colors in comments, and it
can't see rgb()/hsl(), 3/4/8-digit hex, or tell a leftover from a token
definition.
stylelint can do it, but needs Node + a config + postcss/custom-syntax, it
can't see hex inside TSX/CSS-in-JS string literals, and — critically — its
color-no-hex flags your tokens.css (the one file that's supposed to hold
raw colors) exactly as loudly as a stray hex in a component. The request to
exempt token definitions has sat unimplemented for years.
hexsweep sits in between. It's a one-shot, zero-config scanner that is:
- comment-aware — colors in
//,/* */,<!-- -->are skipped (but strings are kept, so it does catch CSS-in-JS likestyled.div`color:#3f82f0`); - definition-aware —
--primary: #3f82f0,$brand: …,@brand: …are allowed by default; only usages are flagged (use--strictto flag definitions too); - structural —
#fff {id-selectors andurl(#a1b2c3)are not colors; - precise — matches hex (3/4/6/8) +
rgb()/rgba()/hsl()/hsla(), never a 5- or 7-digit run, with an--allowlist for the colors you keep on purpose.
Usage
hexsweep # scan the current directory
hexsweep src/ styles/ # scan specific paths
hexsweep --allow "#000,#fff" src # allowlist colors that are OK to hardcode
hexsweep --strict # also flag token definitions (--x/$x/@x)
hexsweep --ext css,scss,vue # override the scanned extensions
hexsweep --json # machine output (byte-identical both builds)
node_modules, .git, dist, build, coverage (and more) are skipped by
default; add others with --exclude. The default extensions are css, scss,
sass, less, vue, svelte, jsx, tsx, js, ts, html, htm, astro.
Exit codes: 0 clean · 1 hardcoded colors found · 2 error.
In CI
- run: pipx run hexsweep src/ # fails the build on a hardcoded color
The --json shape is sorted by (file, line, column) so it's deterministic
regardless of filesystem order:
{
"version": 1,
"summary": { "filesScanned": 24, "filesWithFindings": 1, "findingCount": 2 },
"findings": [
{ "file": "src/components/Button.tsx", "line": 14, "column": 18,
"literal": "#3f82f0", "category": "hex6", "property": "background", "isDefinition": false }
]
}
How it works
It's a line scanner, not a CSS/JS parser — that's what keeps it zero-dependency and lets the Node and Python builds stay byte-identical. It blanks comment spans (keeping string contents and every column position intact), then matches color literals with explicit-ASCII regexes, and applies the id-selector / url() / definition / allowlist gates. Columns are counted in UTF-8 bytes and files are visited in a fixed byte-sorted order, so output never depends on the OS or filesystem.
Scope
MVP matches #hex and rgb()/hsl() functional notation. Named colors (red),
modern oklch()/lab(), autofix, and a config file are intentionally left out —
the goal is a high-signal, zero-config CI gate, not a parser.
Install
pip install hexsweep # or pipx run hexsweep
npm i -g hexsweep # Node build, identical behaviour
Python ≥ 3.8 or Node ≥ 18. No dependencies.
License
MIT
Release files for hexsweep 0.1.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 | |
|---|---|---|---|
| hexsweep-0.1.0.tar.gz | 13.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hexsweep-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 25.7 kB
Release files / hexsweep-0.1.0.tar.gz
| Download URL | hexsweep-0.1.0.tar.gz |
|---|---|
| Size | 13.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
717e7ad880094be2eba073512fbbd022f9d22a9d8e76fd05f77d878105b66fad
|
|
BLAKE2b-256 checksum How to use checksums |
2e45028d5e199d720f2d0a318d7213ae4b0b707d8bb8e7a22708fa6871515a7b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.7
|
Release files / hexsweep-0.1.0-py3-none-any.whl
| Download URL | hexsweep-0.1.0-py3-none-any.whl |
|---|---|
| Size | 11.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
46e19971b2ef50c5cf156e541597cbafc24f27a4c65cfe722b48dd102237d3e2
|
|
BLAKE2b-256 checksum How to use checksums |
2ecfeded61d1c5984f794890902978e591007c9ddb23cb426dfd32506a36dfe7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.7
|