Vale
Config keys: https://vale.sh/docs/keys/. Current Vale 3.x key set is: BasedOnStyles, BlockIgnores, CommentDelimiters, IgnoredClasses, IgnoredScopes, MinAlertLevel, Packages, SkippedScopes, StylesPath, TokenIgnores, Transform, Vocab. Anything else is stale.
Comply, don't demote
When a loaded rule fires, rewrite the prose to comply — don't demote or disable the rule. A firing rule is usually a real style nit worth adopting; restoring a package default typically costs zero current findings and often surfaces a legitimate fix (Microsoft.Contractions catching does not → doesn't).
Demotion (= suggestion), disable (= NO), and scoping are the last resort, reserved for rules wrong for the writing style itself:
Microsoft.Dasheswanting tight em-dashes when the project's style uses spaced.Microsoft.HeadingColonsdemanding sentence case after a colon when ADR/Nygard convention differs.
Confirm the rule contradicts an intentional style choice before silencing it — inconvenience isn't enough. Tuning from the issue framing ("Vale fights writing, silence the noisy rules") rather than the actual per-finding cost is the failure mode this guards against.
Defaults
StylesPath = .vale/styles
MinAlertLevel = warning
Packages = Microsoft, proselint, alex
Vocab = <Project>
[*.md]
BasedOnStyles = Vale, Microsoft, proselint, alex
StylesPath— relative to.vale.ini..vale/styleskeeps the tree hidden. Commit<StylesPath>/config/vocabularies/; gitignore the auto-downloaded package subdirectories Vale writes under<StylesPath>/onvale sync. Migration: Vale 2.x put vocabularies under<StylesPath>/Vocab/; 3.x moved to<StylesPath>/config/vocabularies/.MinAlertLevel—suggestion(Vale default),warning,error. Gates display only; Vale exits nonzero whenever any error-severity finding exists regardless of this setting. Set it to the lowest level you want printed on interactivevale .runs; pass--minAlertLevel=errorto pre-commit to suppress non-error noise from hook output.Packages— comma-separated. Names fromvale-cli/packages, URLs to a.zip, or local paths. Resolvable bare names today:Microsoft, Google, write-good, proselint, Joblint, alex, Readability, RedHat, AsciiDoc, OpenShiftAsciiDoc, Elastic, NoAnimalViolence(style packages), plusHugo, MDX(config packages that teach Vale about non-standard markup, not rule sets). Local files under<StylesPath>/override packages; laterPackagesentries override earlier.Vocab— see Vocabularies below. References<Project>so create the directory plusaccept.txt/reject.txton greenfield.[*.md]BasedOnStyles— without a per-format section, no rules fromPackagesactually run and Vale silently reports zero findings. Add per format you lint ([*.{md,mdx}],[*.rst]). The built-inValestyle must be listed forVale.Terms,Vale.Avoid,Vale.Spellingto fire. Disable individual rules withStyle.Rule = NO.
Choosing Packages
The Defaults set above (Microsoft, proselint, alex) suits technical docs, READMEs, and Diátaxis pages. For that case add two disables to suppress known overlap:
proselint.Cliches = NO # overlaps Microsoft.Terms
proselint.Spelling = NO # Vale.Spelling owns this
Microsoft beats Google as the default base because Microsoft.Avoid+Microsoft.Terms is a longer banned-term list than Google.WordList, defaults to error on bans (matches pre-commit gating), and adds Adverbs/Wordiness rules Google lacks. Every Google rule worth keeping (Passive, Headings, We, FirstPerson, Acronyms, Contractions) duplicates a Microsoft one — see Overlaps below.
Long-form prose / blog. Add write-good and Readability; disable the duplicate Passive.
Packages = Microsoft, proselint, write-good, alex, Readability
[*.md]
BasedOnStyles = Vale, Microsoft, proselint, write-good, alex, Readability
Microsoft.Passive = NO # write-good.Passive at warning is the keeper
Readability.FleschKincaid = YES
Readability adds doc-level grade scoring (per-file, not per-finding); thresholds set in-file. write-good warns on passive (Microsoft's is suggestion) and adds unique ThereIs/Weasel rules. Pair with the en_GB Hunspell pattern (Troubleshooting) for British prose.
Short marketing copy / landing pages. Google over Microsoft here — sentence-case headings fire at warning, Slang/Will rules suit the register. Drop proselint (too noisy on brevity-and-punch copy).
Packages = Google, alex
[*.{md,mdx}]
BasedOnStyles = Vale, Google, alex
Job descriptions (careers/*.md). Joblint adds 14 unique rules (Bro, Meritocracy, Visionary, Benefits, …) absent elsewhere.
Packages = Microsoft, alex, Joblint
[careers/*.md]
BasedOnStyles = Vale, Microsoft, alex, Joblint
Joblint.Gendered = NO # alex.Gendered substitutes; Joblint just blocks
Known overlaps
Loading two packages that fire on the same span produces duplicate findings under different rule names. Don't load both unless deviating intentionally:
Microsoft.Passive≈Google.Passive≈write-good.Passive— same regex; same span fires 2–3×. Pick one (write-good warns; the others suggest).Microsoft.Acronyms≡Google.Acronyms— identical regex and exception list.Microsoft.Headings≈Google.Headings— same scope/match; Google atwarningvs Microsoft'ssuggestion.Microsoft.ContractionsandGoogle.Contractionsboth prefer contractions (not opposite stances).Microsoft.Avoid/Terms⊃Google.WordList;proselint.Clichesoverlaps both.Microsoft.GenderBias≈Google.GenderBias≈proselint.GenderBias— same direction, overlapping token lists.Microsoft.Foreign≡Google.Latin— both replacee.g./i.e..
Deviation triggers
- Repo has
careers/or hiring copy → addJoblint, scope via[careers/*.md]. - Long-form blog or essays → add
Readability+write-good; disableMicrosoft.Passive. - en_GB / en_AU / en_CA prose → see Troubleshooting (vendor Hunspell, disable
Vale.Spelling). - Sentence-case headings wanted as blocking, not suggestion → swap
MicrosoftforGoogle, or keep Microsoft and lower the pre-commit threshold to--minAlertLevel=warningfor that repo. - External contributors with varied English fluency → relax most
proselint.*rules (they default toerror); keepalex— its substitution suggestions teach rather than block. - Inclusive-language gating only, no voice/tone friction → drop Microsoft/Google; load
alexalone (~11 warnings, all actionable). - Per-sentence length nags add noise on non-tutorial prose → disable
Microsoft.SentenceLength, lean onReadabilityfor document-level grade.
Scoping & ignores
Default package rules trip on inline code, tables, and technical strings. Scope them out without disabling rules wholesale:
IgnoredScopes— inline HTML tags Vale skips entirely. Defaults tocode, tt. Addkbd, varif used.SkippedScopes— block HTML tags Vale skips. Defaults toscript, style, pre. Addfigure, blockquotefor untrimmed quoted sources.BlockIgnores/TokenIgnores— regex escape hatches for block and inline content with no HTML tag. Markdown, reStructuredText, AsciiDoc, Org only. Use for fenced shell prompts, custom MDX directives, file paths.IgnoredClasses— by HTML class. Useful for rendered output linting.CommentDelimiters— comment markers Vale honours for<!-- vale off -->directives. Default<!-- -->; set to{/* */}for MDX where HTML comments don't render.BasedOnStylesis additive across sections, not overriding: a child block'sBasedOnStyles = X, Ydoesn't removeZinherited from a broader block'sBasedOnStyles = X, Y, Z. To silence a package's rules inside a sub-block, disable each rule explicitly (Readability.LIX = NO), not by dropping it from the child's list.- Don't lint machine-generated prose. Auto-managed files (release-please
CHANGELOG.md, changesets entries, towncrier fragments) are dense and structured by design. Scope them out with a[CHANGELOG.md]block disablingReadability.*and the stylisticMicrosoft.*rules (Contractions,FirstPerson,HeadingColons,Dashes).
Vocabularies
Vocab = <Project> enables two implicit rules sourced from <StylesPath>/config/vocabularies/<Project>/:
accept.txt→Vale.Terms. Enforces exact casing; if the file listsDiátaxis, thendiataxisbecomes an error.reject.txt→Vale.Avoid. Flags banned terms.
Both files: one regex per line, case-sensitive (prefix (?i) for case-insensitive), # for comments. The built-in Vale style must be in BasedOnStyles for these rules to fire.
Backticked tokens skip Vale.Terms, so prefer wrapping a package ID or code symbol in backticks (magpie-root) over whitelisting the bare token in accept.txt. Reserve accept.txt for terms that appear unbackticked in prose.
A starter accept.txt ships next to this skill at ~/.claude/skills/vale/accept.txt with cross-repo terms (project names, host tooling, languages). Copy into <StylesPath>/config/vocabularies/<Project>/ on greenfield; extend per-project.
Validation (pre-commit)
Two hooks, both id: vale. The first runs vale sync to install declared Packages; the second lints.
- repo: https://github.com/errata-ai/vale
rev: <latest>
hooks:
- id: vale
args: [sync]
pass_filenames: false
- id: vale
args: [--output=line, --minAlertLevel=error]
syncfirst — without it, packages declared in.vale.iniaren't installed in the hook's cached env and the lint produces zero findings (silent pass that looks clean).- Vendored styles → drop the sync hook. If the repo commits its packages under
<StylesPath>/and excludes that path from pre-commit,synchas nothing to fetch — keep only the lint hook. --minAlertLevel=error— overrides the file'swarningdefault so only errors block commits.errata-ai/*repos resolve tovale-cli/*on GitHub; vale.sh still publisheserrata-ai/valein the canonical example. Both work — match the upstream docs rather than chase the rename in every repo.- Pair with
markdownlint-cli2for prose-heavy repos. Vale catches voice/usage; markdownlint catches structure (heading hierarchy, link syntax). No overlap; wire as separate hooks.
CI gate (diff-aware, reviewdog)
Pre-commit's --minAlertLevel=error only blocks errors: vale exits nonzero solely on error-severity findings, so warnings and suggestions accrete silently. To gate warning+ severity without a checked-in baseline, pipe vale JSON through reviewdog filtered to the lines the PR changed. New warnings on touched lines block; main's standing debt is ignored.
Pipeline: vale --output=JSON <set> → jq to rdjsonl → reviewdog -f=rdjsonl -filter-mode=added -fail-level=warning.
name: Vale
on:
pull_request: # added-lines needs a PR diff; reporter needs PR context
permissions:
contents: read
checks: write # github-pr-check writes a check run
pull-requests: write
jobs:
run:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@<sha> # v6
- name: Install vale
run: | # however the repo pins vale; no `vale sync` if styles are vendored
mkdir -p "$HOME/.local/bin"
script/install/vale --bin-dir "$HOME/.local/bin"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- uses: reviewdog/action-setup@<sha> # v1.5.0
- name: Run vale gate
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
vale --output=JSON <set> > vale.json || true
jq -e . vale.json > /dev/null # crash guard: fail if vale emitted no valid JSON
jq -c '
to_entries[] | .key as $p | .value[] | {
message: .Message,
location: {
path: $p,
range: {
start: {line: .Line, column: .Span[0]},
end: {line: .Line, column: (.Span[1] + 1)},
},
},
severity: ({error: "ERROR", warning: "WARNING", suggestion: "INFO"}[.Severity]),
code: {value: .Check},
}' vale.json \
| reviewdog -f=rdjsonl -name=vale \
-reporter=github-pr-check -filter-mode=added -fail-level=warning
- Severity map —
jqtranslates vale'serror/warning/suggestionto reviewdog'sERROR/WARNING/INFO.Span[1] + 1makes the end column exclusive (reviewdog convention). -fail-level=warningfails on warnings and errors. Suggestions map toINFO, surface as non-blockingnoticeannotations, and don't fail; a PR whose only changed-line findings are suggestions concludesneutral, not failure.jq -e .crash guard —vale ... || trueswallows vale's exit code so a real crash doesn't masquerade as "no findings". Thejq -eparse fails the step ifvale.jsonisn't valid JSON.pull_requestonly —-filter-mode=addedneeds a diff, andgithub-pr-checkneeds PR context. Pushes to main keep only the pre-commit error gate; that's intended.vale sync— drop it when styles are vendored under<StylesPath>/; keep it (as a step) only if packages download at CI time.- Reporter choice —
github-pr-checkwrites subtle check-run annotations (used above);github-pr-reviewposts visible threaded review comments. Same gate, louder surfacing — pick per repo's review noise tolerance.
When warranted: a check with both an exit-code gap (the tool won't fail on the severity you care about) and real standing debt on main (a whole-tree gate would be a wall of pre-existing findings). vale's warning/suggestion tiers fit both. Don't bolt it onto a linter that already fails on what you care about and runs clean — that's redundant double-enforcement.
Cleanup cascade
Sweep findings low-tier to high-tier: suggestions, then warnings, then errors. Fixing a suggestion can introduce a new warning or error (a reworded passive becomes a long sentence; an em-dash adjacent to a link trips Microsoft.Dashes via tokenizer normalization). The reverse order risks re-introducing what you just cleared. Suggestions resurfacing during a later pass is fine; warnings or errors resurfacing isn't. Set MinAlertLevel = suggestion in-file to see the full backlog on vale .; keep --minAlertLevel=error in pre-commit so the cascade is opt-in rather than gating commits.
Troubleshooting
vale ls-configprints the resolved config (StylesPath, loaded styles, per-format sections). First stop for "zero findings but my file is wrong."- Zero findings + non-empty
Packages= forgotvale sync, or no[*.<ext>]section declaresBasedOnStyles, or$XDG_CONFIG_HOME/vale/.vale.iniis overriding a single-valued key. Global config loads in addition to project config; multi-valued keys merge, single-valued get overridden. - Excessive findings on a fresh add =
MinAlertLevel = suggestionloading every nit. Raise towarningin-file; keep--minAlertLevel=errorin pre-commit. Packagesname not resolving = wrong casing (write-goodandalexlowercase;Microsoft,Google,Joblint,Readability,RedHatTitleCase) or it's outsidevale-cli/packages— switch to a URL.--filteraccepts a CEL expression for "which rules fired" debugging;--no-wrapdisables output wrapping for grep/CI piping.- British English (en_GB).
Vale.Spellingis en_US. Working pattern: vendorwooorm/dictionariesen_GB Hunspell under<StylesPath>/Custom/, declare aCustom.Spellingrule extendingspellingwithdictionaries: [en_GB], setVale.Spelling = NOper format. Reference exemplar:~/blog.alunduil.com/.vale/styles/Custom/Spelling.yml.
Procedure
- Confirm any field you plan to write against
https://vale.sh/docs/keys/<field>/before editing. Memory and prior commits aren't authoritative — Vale 3.x changed several keys vs 2.x. - Read
.vale.iniif present. NoteStylesPath, declaredPackages, per-formatBasedOnStylesblocks, and anyVocabreferences. - Greenfield — identify the project type (technical docs, long-form prose, marketing copy, job descriptions) and pick the matching recipe from Choosing Packages. Write
.vale.iniwith that recipe; fill<Project>. Copy the starteraccept.txtfrom~/.claude/skills/vale/accept.txtinto<StylesPath>/config/vocabularies/<Project>/; add an empty siblingreject.txt. Add the two-hook pre-commit entry. Runvale synclocally to confirm packages resolve. - Audit existing — walk the Defaults block and flag drift:
[*]instead of[*.<ext>](fires on code files withoutCommentDelimiters)- Silent passes from missing
BasedOnStyles - Declared
Packageswithoutvale syncin pre-commit Vocabreferenced from.vale.inibut missing on disk- Vale 2.x
<StylesPath>/Vocab/path (migrate to<StylesPath>/config/vocabularies/) - Disabled rules without an inline comment explaining why
MinAlertLevel = suggestiondriving pre-commit noise- Packages loaded with known overlap (Microsoft + Google, Microsoft + write-good, proselint + write-good) without the duplicate-rule disables — see Choosing Packages
- Surface findings before editing. Apply only after scope is agreed.