vale
Audit, write, or revise .vale.ini, or work through vale findings on a repo. Use when adding Vale, troubleshooting silent passes or noisy findings, evolving config, or applying the suggestion → warning
SKILL.mddot_claude/skills/vale/SKILL.md
GitHubVale
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, for fenced shell prompts and custom MDX directives. Markdown, reStructuredText, AsciiDoc, Org only. Both belong inside a format section; at the top level Vale refuses to start withE201: 'BlockIgnores' is a syntax-specific option.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).
What markdown tokenises as prose
Which fix a finding admits — backticks, vocabulary, or rewording — depends on what Vale reads as prose:
- Link text is prose; destinations and autolinks aren't. A bare path used as link text gets tokenised as words, and hyphens are word boundaries, so
[scripts/truenas-takeout-extract.sh](../scripts/truenas-takeout-extract.sh)firesVale.TermsontruenasandMicrosoft.Wordinessonextract. Backticking the link text clears both. Paths need noBlockIgnores/TokenIgnoresentry — the destination was never linted. - YAML frontmatter is prose. Vale lints
description:and every other string field. Backticks are literal characters there rather than a code span, so they don't exempt a token, and neitherBlockIgnoresnorTokenIgnoresreaches the block. Reword the field, or add the term toaccept.txt.
Vocabularies
Vocab = <Project> enables two implicit rules sourced from <StylesPath>/config/vocabularies/<Project>/:
accept.txt→Vale.Terms.reject.txt→Vale.Avoid. Flags banned terms.
Both files: one regex per line, case-sensitive, # 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) and the casing convention in its header. Copy into <StylesPath>/config/vocabularies/<Project>/ on greenfield; extend per-project.
Casing
Vale.Terms enforces whatever casing a pattern carries, so the canonical form decides which kind a term is:
- Canonical form carries a capital → bare literal.
TrueNAS,Diátaxis,ADRs?. A lowercasediataxisis then an error. - All-lowercase canonical form →
(?i). The only casing variation such a term meets is its own sentence-initial capital, which is correct English. A literalpre-committurns "Pre-commit hooks must pass" into an error. Fold inflections into the same pattern:(?i)repos?. - One entry per term. Carrying both
alunduilandAlunduilmakes Vale pick one canonical form and error on every use of the other.
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.