How to Build Harper: An Offline Grammar Checker Without Data Leaks
When corporate security policies block external AI tools like Grammarly, developers often find themselves in a tough spot. Leaving typos in technical documentation or code comments hurts credibility when deployed, but manually proofreading everything line by line feels like a huge waste of time.
Harper solves this problem cleanly. Developed in Rust, it is an offline-only grammar checking engine that completely blocks communication with external servers. Unlike LanguageTool, which runs on Java and consumes hundreds of megabytes of memory, Harper uses only around a dozen megabytes. Operating with sub-10ms response times, it ensures your editor won't slow down.
1. Setting Up the Development Environment and Integrating Editor LSP
harper-ls performs syntax analysis entirely within your local machine using only local CPU resources. Simply install the binary via the package manager for your operating system and register it as a standard LSP in your editor.
Package Installation
Run the command in your terminal to install the binary.
- macOS / Linux:
brew install harper
- Rust Environment:
cargo install harper-ls --locked
- Windows:
scoop install harper
Neovim and VS Code Integration
In Neovim, configure target file types and linter rules using nvim-lspconfig.
`lua
local lspconfig = require('lspconfig')
lspconfig.harper_ls.setup({
filetypes = { 'markdown', 'gitcommit', 'rust', 'go', 'typescript', 'python' },
settings = {
["harper-ls"] = {
userDictPath = "~/config/harper/user_dict.txt",
workspaceDictPath = ".harper-dictionary.txt",
linters = {
SpellCheck = true,
SpelledNumbers = false,
AnA = true,
SentenceCapitalization = false,
UnclosedQuotes = true,
WrongApostrophe = false,
LongSentences = true,
RepeatedWords = true,
Spaces = true,
CorrectNumberSuffix = true
}
}
}
})
`
When using the native LSP API in Neovim 0.11 or later, configure vim.lsp.config['harper'] and call vim.lsp.enable('harper'). For VS Code users, simply install the elijah-potter.harper extension and set "harper.path": "/usr/local/bin/harper-ls" in .vscode/settings.json.
Excluding Executable Code and Controlling Inspection Scope
Harper features a built-in Tree-sitter AST parser, allowing it to skip actual source code and selectively inspect only English text inside comment blocks. If you want to completely exclude a specific function's comments from inspection, insert an inline directive.
`javascript
// harper:ignore
function processInternalSecurityToken() {
// spellcheck:ignore
// Internal security token logic
}
`
2. Controlling False Positives with Project Custom Dictionaries
Running only the default English dictionary will flag technical terms like gRPC, OAuth2, and Prometheus as errors. Using a hierarchical dictionary structure helps quickly filter out warning noise.
4-Tier Dictionary Structure
Harper validates words across four layers.
| Dictionary Layer |
Storage Location |
Purpose |
| Static Dictionary |
Embedded in harper-ls binary |
Read-only base English dictionary DB |
| User Dictionary |
~/.config/harper-ls/dictionary.txt |
Global dictionary for personal dev environments |
| Workspace Dictionary |
Project root .harper-dictionary.txt |
Project-specific glossary (Git managed) |
| File-Local Dictionary |
Saved in OS data path |
Single-file identifier storage |
Registering and Sharing Domain Terms
- Create a
.harper-dictionary.txt file in the project root directory.
- Add frequently used technical terms separated by line breaks.
`text
Kubernetes
gRPC
OAuth2
OpenTelemetry
Prometheus
mTLS
Netty
Etcd
`
- Don't panic if a false positive warning pops up in your editor. Simply trigger Code Action (VS Code:
Ctrl + ., Neovim: Code Action keybinding) to add the word directly to .harper-dictionary.txt.
As long as this file is committed to your Git repository, all team members will share the exact same word list.
Adjusting Linter Rules
Disable cumbersome rules when writing comments.
SentenceCapitalization: Set to false to disable forcing the first letter of comments to be capitalized.
LongSentences: Set to false to disable warnings for long sentences, which are common in technical docs.
SpellCheck and UnclosedQuotes: Keep set to true to catch spelling errors and unclosed quotes.
3. Integrating with GitHub Actions CI/CD Pipeline
Typos missed in the editor should be caught at the PR stage. Connecting harper-cli, the CLI tool, to your pipeline automatically filters out grammar errors before merging into the main branch.
Configuring the GitHub Actions Workflow
Define a job in .github/workflows/harper-lint.yml to selectively check only changed Markdown files.
`yaml
name: Technical Documentation Linting
on:
pull_request:
paths:
- 'docs/'
- '.md'
jobs:
harper-grammar-check:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Rust Toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache Harper CLI Binary
uses: actions/cache@v3
with:
path: ~/.cargo/bin/harper-cli
key: ${{ runner.os }}-harper-cli-${{ hashFiles('**/Cargo.lock') }}
- name: Install Harper CLI
run: |
if ! command -v harper-cli &> /dev/null; then
cargo install harper-cli --locked
fi
- name: Get Changed Markdown Files
id: changed-files
run: |
git fetch origin ${{ github.base_ref }}
FILES=$(git diff --name-only --diff-filter=AM origin/${{ github.base_ref }} HEAD | grep '\.md$' || true)
echo "files=$FILES" >> $GITHUB_OUTPUT
- name: Run Harper Lint Check
if: steps.changed-files.outputs.files != ''
run: |
ERRORS=0
for file in ${{ steps.changed-files.outputs.files }}; do
echo "Linting $file with Harper..."
harper-cli lint "$file" || ERRORS=$((ERRORS+1))
done
if [ $ERRORS -gt 0 ]; then
echo "Harper validation failed with $ERRORS error(s)."
exit 1
fi
`
If you commit .harper-dictionary.txt at the project root alongside your code, the CI runner will also validate against the exact same dictionary list as your editor. If you are using a static site generator (SSG) like MkDocs or Docusaurus, it is safer to configure harper-cli lint docs/ to execute right before running the build script.