Skip to content
shbernalPublic

About

Flag a character once, catch it everywhere.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

133 Commits

Folders and files

Repository files navigation

charcheck flagging an em dash in Markdown, a zero width space and an em dash in a string literal, fixing the Markdown one, then flagging an em dash in a commit message

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.
  • --staged reads the git index, so unstaged edits never fail a commit.
  • --commit-msg checks the message itself, skipping comment lines and the scissors.
  • --fix rewrites 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.

Why this project?

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.

Install

Requires Node 24 or newer, which loads a charcheck.config.ts with no bundler or loader.

pnpm add -D charcheck

Scopes other than raw need a parser installed beside it, listed in How it works.

Quickstart

  1. Write charcheck.config.js at 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 the strings scope never reads them.

  2. Add a script and run it:

    { "scripts": { "lint:chars": "charcheck" } }
    pnpm run lint:chars
  3. Rewrite what the rules can fix, then read the diff:

    pnpm run lint:chars --fix
  4. Call it from your git hooks. The pre-commit hook runs charcheck --staged and the commit-msg hook runs charcheck --commit-msg "$1". Getting started has the setup for lefthook, husky, plain .git/hooks and CI.

    One charcheck.config.js drives the package script, the pre-commit hook, which reads the git index and ignores unstaged edits, and the commit-msg hook, which skips comment lines and the scissors

How it works

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]
Loading

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.

The same TypeScript and Markdown files under each scope: raw flags every em dash, comments and strings flag only the ones in comments or literals, markdown flags only the one in prose
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.

For agents

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 -y

Agent skill covers updating it and what --report-issue prints.

More

About

Flag a character once, catch it everywhere.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages