How to automate style checks with Vale: a complete guide (October 2026)

12 min read

Style guides fail quietly. A team writes down its rules for product names, headings, and tone, and within a few months the docs drift away from them because enforcement depends on whoever reviews the pull request remembering every rule. Vale fixes the enforcement half: it's an open source prose linter that checks Markdown, MDX, AsciiDoc, and other formats against rules written as YAML, so style checks run the same way on every change. Getting Vale installed takes minutes. Getting it automated so that contributors trust its output takes a decision most guides skip: which rules block a merge and which only advise. This guide treats that severity decision as the core of the setup and walks through installation, house rules, CI, and pre-commit hooks around it, so that the docs-as-code workflow enforces your style guide instead of just storing it.

TLDR:

  • Vale is an MIT-licensed prose linter written in Go. A .vale.ini file chooses which styles apply to which files, and vale sync downloads style packages such as Microsoft, Google, RedHat, write-good, and proselint.
  • House rules are YAML files built on extension points like existence, substitution, capitalization, and consistency, and vocabularies (accept.txt and reject.txt) manage product terms without disabling spell check.
  • Only error-level alerts make Vale exit with code 1, so the severity you assign each rule decides what fails CI. Promote a rule to error only when it has no false positives on your current docs.
  • The vale-cli/vale-action GitHub Action runs vale sync and Vale on pull requests and comments on changed lines, and the pre-commit framework catches the same issues before a commit is made.
  • Vale 3.18 and later parses MDX natively, ignoring JSX, imports, and code, and comments like {/* vale off */} switch checks off for specific blocks.
  • Fern docs are MDX in Git, and Fern Editor and Fern Agent edits arrive as pull requests, so one Vale workflow checks writing from engineers, non-technical contributors, and AI agents alike.

What Vale checks and how it works

Vale is a command-line linter for prose. Like ESLint for JavaScript or Spectral for OpenAPI files, it reads files, applies a set of rules, and reports violations with a file, line, column, severity, and rule name. It doesn't ship with an opinion about writing. Every rule comes from a style, which is a folder of YAML rule files on the StylesPath.

Vale understands markup. In Markdown and MDX, it skips fenced code blocks and inline code spans, so a variable named utilize_cache doesn't trigger a "use 'use' instead of 'utilize'" rule. It lints headings, paragraphs, list items, table cells, and front matter as separate scopes, which lets a rule target only headings or only body text.

Vale runs locally, so nothing is sent to an external service. It is distinct from grammar tools like Grammarly or LanguageTool: those apply general writing advice, while Vale enforces the rules your team writes, such as "the product is called Fern Editor" or "headings use sentence case."

Step 1: install Vale and create a configuration file

Install the binary with a package manager (brew install vale on macOS, choco install vale on Windows, or a release binary or Docker image on Linux), then confirm it runs with vale --version.

Vale looks for its configuration starting in the working directory and searching upward, so put a .vale.ini at the repository root. A starting point for a docs repository:

StylesPath = .github/styles
MinAlertLevel = suggestion
Packages = Microsoft
Vocab = Docs
 
[*.{md,mdx}]
BasedOnStyles = Vale, Microsoft, House

Each line does one job:

  • StylesPath is where downloaded packages and your own styles live.
  • MinAlertLevel is the lowest severity Vale reports. The --minAlertLevel flag overrides it per run.
  • Packages lists the style packages vale sync installs.
  • Vocab loads a vocabulary from <StylesPath>/config/vocabularies/Docs/.
  • [*.{md,mdx}] is a glob section. BasedOnStyles lists the styles that run on matching files, where Vale is the built-in style that powers spelling and vocabulary checks, Microsoft is the package, and House is your custom style.

On Vale 3.18 or later, MDX needs no extra configuration. Earlier releases either required the external mdx2vast parser or a [formats] section mapping mdx = md, which treats MDX as plain Markdown and can misread JSX. Pin a current release in every environment so local runs and CI behave the same way. vale ls-config prints the resolved configuration as JSON, which is the fastest way to debug a rule that isn't firing.

Step 2: choose a base style package

Writing every rule from scratch is slow. Most teams start from a published style guide and override what doesn't fit. The Vale package library includes:

PackageBased onGood fit for
MicrosoftMicrosoft Writing Style GuideProduct and API docs with a direct, second-person voice
GoogleGoogle developer documentation style guideDeveloper docs and API references
RedHatRed Hat supplementary style guideEnterprise and open source technical docs
write-goodThe write-good linterCatching passive voice, weasel words, and filler
proselintThe proselint projectGeneral usage errors and clichés
alexThe alex linterFlagging insensitive or exclusionary language
ReadabilityStandard readability formulasGrade-level targets for tutorials and guides

Run vale sync after changing Packages. Packages are rebuilt from the configuration on every sync, so commit only what you wrote (your House style and the config/ directory) and ignore the downloaded folders in .gitignore.

Expect a large first run. A mature docs repository linted against the full Microsoft style often produces hundreds of alerts. Don't fix them all at once. Work through them rule by rule and decide, for each, whether to fix the docs, downgrade the rule, or switch it off with a line like Microsoft.Passive = NO in the glob section.

Step 3: encode your house rules

A base package covers general writing. Your own style covers the rules that are specific to your product, which are usually the ones reviewers miss. Create a folder named after the style (for example .github/styles/House/) and add one YAML file per rule.

A substitution rule enforces canonical product names and replaces retired terms:

# .github/styles/House/Terms.yml
extends: substitution
message: "Use '%s' instead of '%s'."
level: error
ignorecase: true
swap:
  web hook: webhook
  sign-on: sign in
  whitelist: allowlist
  blacklist: denylist

An existence rule flags phrases that have no correct form in your docs:

# .github/styles/House/Hedging.yml
extends: existence
message: "Remove '%s' and state the behavior directly."
level: warning
ignorecase: true
tokens:
  - simply
  - just
  - it should be noted
  - basically

Other extension points cover structural rules. capitalization enforces sentence case or title case on headings, consistency flags a file that mixes two spellings of one term (like "email" and "e-mail"), occurrence limits how often a pattern appears, conditional checks that an acronym is defined before it's used, and metric and readability score whole documents. A rule can also carry a link to the section of your style guide it enforces, which turns every alert into a pointer to the reasoning.

Use vocabularies for product terms

Spell check is the first rule to break in technical docs, because product names, CLI flags, and API terms aren't dictionary words. The tempting fix is to switch off Vale.Spelling. The better fix is a vocabulary. Add accepted terms, one regular expression per line, to config/vocabularies/Docs/accept.txt:

Fern
OpenAPI
[Ww]ebhooks?
(?i)kubectl

Accepted terms become exceptions for every style in BasedOnStyles, and they also feed the built-in Vale.Terms rule, which enforces their exact casing. Writing Openapi then produces an error with a suggested fix. Terms in reject.txt are flagged as errors everywhere through Vale.Avoid. One vocabulary file keeps product terminology consistent across every style without editing each one.

Step 4: decide which rules block a merge

This is the step that decides whether the automation survives. Vale's exit codes are simple: 0 when no error-level alert was found, 1 when at least one was, and 2 when Vale itself couldn't run, such as a missing configuration file or a rule that failed to load. Warnings and suggestions never change the exit code. That makes each rule's level a policy choice about what stops a pull request.

If too many rules are errors, contributors learn that CI fails for reasons that don't matter, and they start disabling rules inline or ignoring the check. If too few are errors, Vale becomes a report nobody reads. A severity model that holds up in practice:

  • error for rules with no false positives on your current docs: product names, retired terms, banned words, and vocabulary casing. These block the merge.
  • warning for rules that are usually right but need judgment, such as hedging words or heading capitalization in a repository with legacy pages. These show up as review comments.
  • suggestion for advisory rules like passive voice or readability scores. These are useful in an editor and too noisy in CI.

Override package severities in .vale.ini instead of editing the package files, so vale sync doesn't overwrite your choices:

[*.{md,mdx}]
BasedOnStyles = Vale, Microsoft, House
Microsoft = warning
Microsoft.Headings = error
House.Hedging = suggestion

Promote rules gradually. Start with only the vocabulary and product-name rules at error, fix the backlog for one rule, promote it, and repeat. Each promotion is a small, reviewable change, and the team never faces a red CI run caused by hundreds of pre-existing violations. For a one-off audit at a different level, vale --filter='.Level == "error"' docs/ runs only the rules that would block a merge.

Step 5: run Vale on every pull request

Local runs depend on each contributor remembering to run them. CI makes the check unconditional. The Vale GitHub Action installs Vale, runs vale sync by default, lints the files, and reports results through reviewdog:

# .github/workflows/vale.yml
name: Vale
on:
  pull_request:
    paths:
      - "**.md"
      - "**.mdx"
      - ".vale.ini"
      - ".github/styles/**"
 
permissions:
  contents: read
  pull-requests: write
 
jobs:
  vale:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: vale-cli/vale-action@v3
        with:
          version: 3.25.1
          reporter: github-pr-review
          filter_mode: added
          fail_on_error: true

The settings that matter:

  • filter_mode: added (the default) reports only alerts on lines the pull request changed. This is what lets you adopt Vale on an existing docs set without fixing the whole backlog first.
  • reporter: github-pr-review posts alerts as inline review comments, with suggested changes where a rule carries a fix.
  • fail_on_error: true fails the job on error-level alerts. Leave it false for a report-only rollout period.
  • version pins the Vale release, so a new version doesn't change results between runs.

The paths filter also re-runs the check when the configuration or styles change, so a stricter rule is tested against the pull request that introduces it. Pull requests from forks get a read-only token, so review comments aren't available there, though the action's default annotations reporter still flags issues. Teams on GitLab CI, Jenkins, or another runner can use the jdkato/vale Docker image and rely on the exit code directly.

Step 6: catch issues before commit and in the editor

CI is the backstop, and earlier feedback is cheaper. Vale publishes a hook for the pre-commit framework:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/vale-cli/vale
    rev: v3.25.1
    hooks:
      - id: vale
        name: vale sync
        pass_filenames: false
        args: [sync]
      - id: vale
        args: [--output=line, --minAlertLevel=error]

Setting --minAlertLevel=error keeps the hook to the same rules that block a merge, so commits aren't interrupted by advisory alerts. Pin rev to a release tag rather than a commit hash. Husky and lefthook can run the same command for JavaScript-heavy repositories.

In the editor, the Vale Language Server (vale-ls) provides inline diagnostics and quick fixes in VS Code, Neovim, and any editor that supports the Language Server Protocol. Writers see suggestion-level alerts while drafting, which is where those advisory rules belong.

Handling false positives in technical documentation

False positives are what make teams abandon a linter, so handle them deliberately rather than with inline overrides scattered across files.

  • Prefer vocabularies over disabled rules. A term that trips spell check belongs in accept.txt, not in a Vale.Spelling = NO line that also hides real typos.
  • Use comments for the exceptions. In MDX, {/* vale off */} and {/* vale on */} disable checks for a block, and {/* vale House.Terms = NO */} disables one rule. In Markdown, the equivalent is an HTML comment such as <!-- vale off -->. Reserve these for quoted material, like a customer quote or an error message that must appear verbatim.
  • Scope rules by path. A later glob section such as [changelog/**] can switch off rules that don't fit release notes, and a section for translated content can drop English-only rules. BasedOnStyles = NONE disables linting for a path entirely.
  • Ignore generated content. API reference pages generated from an OpenAPI spec don't need prose linting in the docs repo, since descriptions should be fixed in the spec. Keep them out of the glob or lint the spec descriptions separately.
  • Test your rules. Vale 3.24 added vale test, which runs test cases stored next to each rule and confirms that it fires on the inputs it should and stays silent on the ones it shouldn't. A rule with a broken regex otherwise loads, matches nothing, and reports success.

Common mistakes when automating Vale

  • Turning on every package rule at error on day one. The first pull request fails for reasons unrelated to its change, and the team learns to ignore the check.
  • Committing downloaded packages. They go stale and conflict with vale sync. Commit your own style and vocabulary, and sync the rest.
  • Leaving Vale unpinned. A new release can parse a file differently, and an unpinned CI run starts failing without any change to the docs.
  • Linting only in CI. Contributors who see alerts only after pushing make more round trips than those who see them in the editor or pre-commit hook.
  • Holding AI-generated content to different rules. Docs written or edited by AI agents should pass the same checks as human writing. Running Vale on every pull request, regardless of who opened it, is the simplest way to guarantee that.

Vale handles prose. It pairs well with structural checks such as link checking and MDX syntax validation, covered in more depth in this docs linting guide.

Automating style checks in Fern docs

Fern docs are Markdown and MDX files in a Git repository, so Vale works on them without adapters. Fern's Vale setup guide covers running it against the fern/ folder, disabling checks around code examples with MDX comments, and automating it with the GitHub Action or pre-commit. The workflow above covers every contributor because every change reaches the repository as a pull request: the Fern Editor opens a pull request for each visual edit, and Fern Agent opens pull requests for edits requested from the Fern Dashboard, Slack, or a coding agent. Vale runs on those the same way it runs on an engineer's branch. For structural checks, the Fern CLI's fern docs md check validates MDX syntax, and fern check validates docs.yml, with rules such as broken links set to warn or error through check.rules.

Final thoughts on automating Vale style checks

Vale's mechanics are simple: a configuration file, a few style packages, a folder of YAML rules, and a CI job. What makes the automation last is the severity model. Block merges only on rules with no false positives, show judgment calls as review comments, keep advisory rules in the editor, and promote rules one at a time as the backlog clears. Contributors keep trusting a check that fails only for real problems, and that trust is what turns a written style guide into one the docs actually follow.

To see how Fern's Git-based docs, visual editor, and AI agent fit into a style-checked pull request workflow, book a demo.

FAQ

Is Vale free to use?

Yes. Vale is open source under the MIT license and runs as a single binary on macOS, Windows, and Linux, with no account or external service required. Its official style packages, the GitHub Action, and the pre-commit hook are free as well.

How do you make Vale fail a CI build?

Set the rules that should block merges to level: error, either in the rule file or with an override like House.Terms = error in .vale.ini. Vale exits with code 1 only when an error-level alert is found, so CI fails on those and passes on warnings and suggestions. In the GitHub Action, also set fail_on_error: true.

Does Vale work with MDX files?

Yes. Vale 3.18 and later parses MDX natively, ignoring JSX tags, import and export statements, JavaScript expressions, and code, while still linting the Markdown inside JSX children (from Vale 3.19). Older versions need the mdx2vast parser or a [formats] mapping of mdx = md.

What is the difference between Vale and markdownlint?

markdownlint checks Markdown syntax and formatting, such as heading levels, list indentation, and trailing spaces. Vale checks the prose itself, such as terminology, tone, word choice, and capitalization. Many docs teams run both, with markdownlint for structure and Vale for style.

Which Vale style should a docs team start with?

The Microsoft and Google packages are the most common starting points for developer documentation, since both are based on published technical writing style guides. Start with one, add a custom house style for product terms, and tune severities after the first run rather than adopting every rule at full strength.