# 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 → error cleanup cascade after a vale run (interactive or pre-commit failure). Applies the two-hook pre-commit pattern (sync + lint), explicit Packages and per-format BasedOnStyles, scoping/ignores for false positives, and a shared accept.txt vocabulary.

- Skill: `alunduil/vale` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add alunduil/vale`
- Raw SKILL.md: https://api.skillmd.com/api/skills/alunduil/vale/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: alunduil (https://skillmd.com/u/alunduil)
- Updated: 2026-08-19
- Page: https://skillmd.com/skills/alunduil/vale

---


# 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.Dashes` wanting tight em-dashes when the project's style uses spaced.
- `Microsoft.HeadingColons` demanding 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

```ini
StylesPath = .vale/styles
MinAlertLevel = warning
Packages = Microsoft, proselint, alex
Vocab = <Project>

[*.md]
BasedOnStyles = Vale, Microsoft, proselint, alex
```

- `StylesPath` — relative to `.vale.ini`. `.vale/styles` keeps the tree hidden. Commit `<StylesPath>/config/vocabularies/`; gitignore the auto-downloaded package subdirectories Vale writes under `<StylesPath>/` on `vale 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 interactive `vale .` runs; pass `--minAlertLevel=error` to pre-commit to suppress non-error noise from hook output.
- `Packages` — comma-separated. Names from `vale-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), plus `Hugo, MDX` (config packages that teach Vale about non-standard markup, not rule sets). Local files under `<StylesPath>/` override packages; later `Packages` entries override earlier.
- `Vocab` — see Vocabularies below. References `<Project>` so create the directory plus `accept.txt`/`reject.txt` on greenfield.
- `[*.md]` `BasedOnStyles` — **without a per-format section, no rules from `Packages` actually run and Vale silently reports zero findings.** Add per format you lint (`[*.{md,mdx}]`, `[*.rst]`). The built-in `Vale` style must be listed for `Vale.Terms`, `Vale.Avoid`, `Vale.Spelling` to fire. Disable individual rules with `Style.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:

```ini
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.

```ini
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).

```ini
Packages = Google, alex

[*.{md,mdx}]
BasedOnStyles = Vale, Google, alex
```

**Job descriptions (`careers/*.md`).** Joblint adds 14 unique rules (`Bro`, `Meritocracy`, `Visionary`, `Benefits`, …) absent elsewhere.

```ini
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 at `warning` vs Microsoft's `suggestion`.
- `Microsoft.Contractions` and `Google.Contractions` both *prefer* contractions (not opposite stances).
- `Microsoft.Avoid`/`Terms` ⊃ `Google.WordList`; `proselint.Cliches` overlaps both.
- `Microsoft.GenderBias` ≈ `Google.GenderBias` ≈ `proselint.GenderBias` — same direction, overlapping token lists.
- `Microsoft.Foreign` ≡ `Google.Latin` — both replace `e.g.`/`i.e.`.

### Deviation triggers

- Repo has `careers/` or hiring copy → add `Joblint`, scope via `[careers/*.md]`.
- Long-form blog or essays → add `Readability` + `write-good`; disable `Microsoft.Passive`.
- en_GB / en_AU / en_CA prose → see Troubleshooting (vendor Hunspell, disable `Vale.Spelling`).
- Sentence-case headings wanted as blocking, not suggestion → swap `Microsoft` for `Google`, or keep Microsoft and lower the pre-commit threshold to `--minAlertLevel=warning` for that repo.
- External contributors with varied English fluency → relax most `proselint.*` rules (they default to `error`); keep `alex` — its substitution suggestions teach rather than block.
- Inclusive-language gating only, no voice/tone friction → drop Microsoft/Google; load `alex` alone (~11 warnings, all actionable).
- Per-sentence length nags add noise on non-tutorial prose → disable `Microsoft.SentenceLength`, lean on `Readability` for 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 to `code, tt`. Add `kbd, var` if used.
- `SkippedScopes` — block HTML tags Vale skips. Defaults to `script, style, pre`. Add `figure, blockquote` for 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.
- `BasedOnStyles` is **additive across sections, not overriding**: a child block's `BasedOnStyles = X, Y` doesn't remove `Z` inherited from a broader block's `BasedOnStyles = 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 disabling `Readability.*` and the stylistic `Microsoft.*` 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 lists `Diátaxis`, then `diataxis` becomes 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.

```yaml
- repo: https://github.com/errata-ai/vale
  rev: <latest>
  hooks:
    - id: vale
      args: [sync]
      pass_filenames: false
    - id: vale
      args: [--output=line, --minAlertLevel=error]
```

- `sync` first — without it, packages declared in `.vale.ini` aren'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, `sync` has nothing to fetch — keep only the lint hook.
- `--minAlertLevel=error` — overrides the file's `warning` default so only errors block commits.
- `errata-ai/*` repos resolve to `vale-cli/*` on GitHub; vale.sh still publishes `errata-ai/vale` in the canonical example. Both work — match the upstream docs rather than chase the rename in every repo.
- Pair with `markdownlint-cli2` for 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`.

```yaml
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** — `jq` translates vale's `error`/`warning`/`suggestion` to reviewdog's `ERROR`/`WARNING`/`INFO`. `Span[1] + 1` makes the end column exclusive (reviewdog convention).
- **`-fail-level=warning`** fails on warnings **and** errors. Suggestions map to `INFO`, surface as non-blocking `notice` annotations, and don't fail; a PR whose only changed-line findings are suggestions concludes **`neutral`**, not failure.
- **`jq -e .` crash guard** — `vale ... || true` swallows vale's exit code so a real crash doesn't masquerade as "no findings". The `jq -e` parse fails the step if `vale.json` isn't valid JSON.
- **`pull_request` only** — `-filter-mode=added` needs a diff, and `github-pr-check` needs 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-check` writes subtle check-run annotations (used above); `github-pr-review` posts 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-config` prints the resolved config (StylesPath, loaded styles, per-format sections). First stop for "zero findings but my file is wrong."
- Zero findings + non-empty `Packages` = forgot `vale sync`, or no `[*.<ext>]` section declares `BasedOnStyles`, or `$XDG_CONFIG_HOME/vale/.vale.ini` is 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 = suggestion` loading every nit. Raise to `warning` in-file; keep `--minAlertLevel=error` in pre-commit.
- `Packages` name not resolving = wrong casing (`write-good` and `alex` lowercase; `Microsoft`, `Google`, `Joblint`, `Readability`, `RedHat` TitleCase) or it's outside `vale-cli/packages` — switch to a URL.
- `--filter` accepts a CEL expression for "which rules fired" debugging; `--no-wrap` disables output wrapping for grep/CI piping.
- **British English (en_GB).** `Vale.Spelling` is en_US. Working pattern: vendor `wooorm/dictionaries` en_GB Hunspell under `<StylesPath>/Custom/`, declare a `Custom.Spelling` rule extending `spelling` with `dictionaries: [en_GB]`, set `Vale.Spelling = NO` per format. Reference exemplar: `~/blog.alunduil.com/.vale/styles/Custom/Spelling.yml`.

## Procedure

1. 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.
2. Read `.vale.ini` if present. Note `StylesPath`, declared `Packages`, per-format `BasedOnStyles` blocks, and any `Vocab` references.
3. **Greenfield** — identify the project type (technical docs, long-form prose, marketing copy, job descriptions) and pick the matching recipe from Choosing Packages. Write `.vale.ini` with that recipe; fill `<Project>`. Copy the starter `accept.txt` from `~/.claude/skills/vale/accept.txt` into `<StylesPath>/config/vocabularies/<Project>/`; add an empty sibling `reject.txt`. Add the two-hook pre-commit entry. Run `vale sync` locally to confirm packages resolve.
4. **Audit existing** — walk the Defaults block and flag drift:
   - `[*]` instead of `[*.<ext>]` (fires on code files without `CommentDelimiters`)
   - Silent passes from missing `BasedOnStyles`
   - Declared `Packages` without `vale sync` in pre-commit
   - `Vocab` referenced from `.vale.ini` but missing on disk
   - Vale 2.x `<StylesPath>/Vocab/` path (migrate to `<StylesPath>/config/vocabularies/`)
   - Disabled rules without an inline comment explaining why
   - `MinAlertLevel = suggestion` driving pre-commit noise
   - Packages loaded with known overlap (Microsoft + Google, Microsoft + write-good, proselint + write-good) without the duplicate-rule disables — see Choosing Packages
5. Surface findings before editing. Apply only after scope is agreed.

