charcheck flags banned characters, like em dashes and zero width spaces, in the parts of a repo you point it at. One config drives the package script, the pre-commit hook and the commit-msg hook. Each rule picks which files it reads and which part of each file it may match inside.
- Scopes that read only string literals, only comments, only Markdown prose, or only the rendered text of a Vue or HTML file.
--stagedreads the git index, so unstaged edits never fail a commit.--commit-msgchecks the message itself, skipping comment lines and the scissors.--fixrewrites what a rule knows how to replace and reports the rest.- A baseline file, so a repo that is not at zero can still go green.
- Pretty, JSON and SARIF output.
Each existing tool sees one surface. ESLint plugins see the JavaScript AST, prose linters see Markdown, and nothing checks commit messages. Banning one character across a repo used to mean three tools, three configs, and a gap where the commit message goes.
| Tool | Reads | Pick it when |
|---|---|---|
| charcheck | Code, comments, Markdown, Vue, HTML, commits | One character list should hold everywhere, per-surface policy |
| eslint-plugin-em-dash-checker | JavaScript string literals | Your text lives only in code and you already run ESLint |
| textlint + prh | Prose files | You want natural language rules, not just characters |
| Vale | Prose, with real style guides | You want an editorial style guide enforced |
| slop-lint | Vocabulary and characters | You want opinions about words too |
charcheck has no word lists and won't get any. It is about characters.
Requires Node 24 or newer, which loads a charcheck.config.ts with no bundler or loader.
pnpm add -D charcheckScopes other than raw need a parser installed beside it, listed in
How it works.
-
Write
charcheck.config.jsat the repo root. This one bans em dashes everywhere, with a different policy for each surface:const DASH = '—'; export default { rules: [ { id: 'prose', chars: [DASH], fix: ',', scope: 'markdown', include: ['**/*.md'] }, { id: 'code', chars: [DASH], scope: 'strings', include: ['src/**/*.ts'] }, { id: 'pages', chars: [DASH], scope: 'markup', include: ['src/**/*.vue'] }, { id: 'commits', chars: [DASH], include: ['<commit-msg>'] }, ], };
Comments in
src/stay free to use the dash, because thestringsscope never reads them. -
Add a script and run it:
{ "scripts": { "lint:chars": "charcheck" } }pnpm run lint:chars
-
Rewrite what the rules can fix, then read the diff:
pnpm run lint:chars --fix
-
Call it from your git hooks. The pre-commit hook runs
charcheck --stagedand the commit-msg hook runscharcheck --commit-msg "$1". Getting started has the setup for lefthook, husky, plain.git/hooksand CI.
flowchart LR
W[working tree] --> R
I["git index (--staged)"] --> R
M["commit message (--commit-msg)"] --> R
R["rule: include globs"] --> S["scope: the part of the file it may read"]
S --> X[match banned characters]
X --> O[report, or --fix]
A rule's scope decides which part of a file it may match inside.
A wrong scope fails silently, because a scan that reads nothing looks like a scan that
passed.
| Scope | Reads | Applies to | Needs |
|---|---|---|---|
raw (default) |
The whole file | Any file | Nothing |
strings |
String and template literals, never comments | JavaScript, TypeScript | typescript |
comments |
Line and block comments, never literals or code | JavaScript, TypeScript | typescript |
markup |
Template text, allowlisted attributes, script blocks | .vue |
@vue/compiler-sfc, typescript |
markdown |
Prose, never fenced or inline code, never link targets | .md, .markdown |
micromark |
html |
Page text, allowlisted attributes, script literals | .html, .htm |
parse5, typescript |
Every parser is an optional peer dependency, imported only when a rule uses its scope.
A repo using only raw installs nothing extra.
TypeScript 5, 6 and 7 all work, except that 7 can't read JSX.
Scopes has the details.
An agent that hits a charcheck bug sees a pass, not an error, and tends to paper over it
with an exclude glob.
The package ships a charcheck-upstream skill that teaches it to triage the miss and file a
real report:
npx skills add ./node_modules/charcheck -s '*' -a claude-code -a codex -a universal -yAgent skill covers updating it and what --report-issue prints.
- Getting started: scripts, hook managers and CI
- Scopes: which part of a file a rule should read
- Configuration: rules, fixes and suppression comments
- Command line: every flag and exit code
- Baseline: adopting charcheck in a repo that is not at zero
- Programmatic API: calling
scanfrom a script or a test - Presets: ready-made character lists
- Limitations: what is not caught, on purpose or not yet
- Contributing and issues