# How to automate style checks with Vale: a complete guide (October 2026) 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](https://vale.sh/) 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](https://buildwithfern.com/post/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: ```ini 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 `/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](https://vale.sh/explorer) includes: | Package | Based on | Good fit for | | --- | --- | --- | | `Microsoft` | Microsoft Writing Style Guide | Product and API docs with a direct, second-person voice | | `Google` | Google developer documentation style guide | Developer docs and API references | | `RedHat` | Red Hat supplementary style guide | Enterprise and open source technical docs | | `write-good` | The write-good linter | Catching passive voice, weasel words, and filler | | `proselint` | The proselint project | General usage errors and clichés | | `alex` | The alex linter | Flagging insensitive or exclusionary language | | `Readability` | Standard readability formulas | Grade-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: ```yaml # .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: ```yaml # .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`: ```text 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: ```ini [*.{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](https://github.com/vale-cli/vale-action) installs Vale, runs `vale sync` by default, lints the files, and reports results through reviewdog: ```yaml # .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](https://pre-commit.com/) framework: ```yaml # .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 ``. 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](https://buildwithfern.com/post/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](https://buildwithfern.com/learn/docs/developer-tools/vale) 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](https://buildwithfern.com/learn/docs/writing-content/fern-editor) opens a pull request for each visual edit, and [Fern Agent](https://buildwithfern.com/learn/docs/ai-features/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`](https://buildwithfern.com/learn/cli-api-reference/cli-reference/docs-commands) validates MDX syntax, and `fern check` validates `docs.yml`, with rules such as broken links set to `warn` or `error` through [`check.rules`](https://buildwithfern.com/learn/docs/configuration/site-level-settings#check-configuration). ## 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](https://buildwithfern.com/book-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.