2. Lint on every commit and every PR
← Previous: Define the flag namespace · Index · Next: Schema-driven application code
exd lint runs the same validator the SDK runs at load time. It catches typos in field names, dangling segment references, mismatched predicate operand shapes, and many things you cannot grep for. Every error gets a stable code (E001–E039) so CI logs and fix scripts are durable. The full diagnostic vocabulary lives in docs/reference/manifest/11-diagnostics.md.
The goal of this chapter: a manifest that doesn't lint should never reach a reviewer.
Run it locally first
From the application repo root:
$ exd lint marketing/
namespace 'marketing': 1 flag, 3 segments — OK
The argument to exd lint is always the flag-namespace directory — the one that contains flags/ and segments/. exd lint . only works if you've cd'd into marketing/ first; running it from the repo root will fail because the repo root isn't a manifest.
See what a failure looks like
Temporarily change one rule's segment reference to a typo — for example, edit flags/welcome-banner.toml and change segment = "welcome-banner-bucket-control" to segment = "welcome-banner-bucket-controll" (two ls). Re-run:
$ exd lint marketing/
error[E005] flag 'welcome-banner' rule[0]: unknown segment `welcome-banner-bucket-controll`
at flags/welcome-banner.toml
1 error, 0 warnings
Exit code is 1. Revert the typo before continuing.
The E005 is one of around 40 diagnostic codes — each with a stable identifier, a fixed message format, and an entry in docs/reference/manifest/11-diagnostics.md. When CI logs say E005, you can grep the diagnostic vocabulary and know exactly what's wrong, even months later.
Wire it into pre-commit
A pre-commit hook stops broken manifests from being committed at all. Two flavours, pick whichever fits your team's setup.
Option A — the pre-commit framework
If your repo already has a .pre-commit-config.yaml, add the exd hook to it:
repos:
- repo: https://github.com/manasgarg/exd
rev: v0.1.0 # pin to a release tag
hooks:
- id: exd-lint
The hook is defined by .pre-commit-hooks.yaml at the root of this repo and runs exd lint whenever staged changes touch flags/, segments/, or namespace.toml. It assumes the exd binary is on PATH.
Install once per clone:
pre-commit install
Now every git commit runs the lint hook against the staged files. Try the typo again — the commit will be rejected before it ever reaches the index.
Option B — a raw git hook
No framework dependency, just a script at .git/hooks/pre-commit:
#!/usr/bin/env bash
set -euo pipefail
# Only run when staged changes touch the flag namespace.
if ! git diff --cached --name-only --diff-filter=ACM \
| grep -qE '^marketing/(flags|segments)/|^marketing/namespace\.toml$'; then
exit 0
fi
if ! exd lint marketing/ --format json > /tmp/exd-lint.json; then
echo "exd lint failed — commit blocked. See /tmp/exd-lint.json"
exit 1
fi
Make it executable:
chmod +x .git/hooks/pre-commit
For multi-namespace repos, docs/workflows/cicd-integration.md § Pre-commit Hook Setup ships a longer version that walks staged changes and lints every touched namespace, not just marketing/.
Wire it into CI on PR
Local hooks are bypassable (git commit --no-verify); CI isn't. Both belong in your setup. Here's a GitHub Actions workflow that runs exd lint on every PR that touches a manifest file:
# .github/workflows/exd-lint.yml
name: exd lint
on:
pull_request:
paths:
- "marketing/flags/**"
- "marketing/segments/**"
- "marketing/namespace.toml"
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo install exd-client --locked
- run: exd lint marketing/ --format json
A few notes on the workflow:
paths:scopes the workflow. Code-only PRs don't pay the CI minute. Addnamespace.tomlhere if you ever start using one.- Pin
exd-clientto a specific version once your manifests start depending on diagnostic codes —cargo install exd-client@0.1.0. CI behavior should not change when the CLI is updated out-of-band. --format jsonemits a structured envelope (seeexd lintJSON output). Feed it into your PR-comment bot if you want diagnostics inline in the review UI.
Reviewer ergonomics
A reviewer's first move on a flag-touching PR should be exd explain (see Chapter 1) — but the second should be a quick visual scan of the lint output. The JSON envelope makes that scriptable. A common pattern: a per-namespace exd lint status check named the same as the namespace slug, so the PR's check list reads exd lint / marketing and reviewers can see at a glance which namespace is implicated.
Next
Chapter 3 — Schema-driven application code. The manifest is now defended against itself; next, defend the application code against silent drift from the manifest.