IntentLint

Examples

Rules a syntax linter cannot see

Write the invariant in English. IntentLint evaluates every pull request against it — including changes written by coding agents. These are the kinds of rules teams check into .intentlint.yml.

Idempotent workers

- id: jobs/idempotent-workers
  description: >
    Background workers that make external
    side effects must be idempotent.
    Retries must not duplicate irreversible
    work such as charges, emails, or remote
    writes.

Why it’s powerful

Idempotency is a behavioral property, not a syntax pattern. Traditional linters can require a decorator or ban a helper, but they cannot tell whether a worker is actually safe to run twice. IntentLint reads the rule and the diff and decides whether retries would duplicate a side effect.

Tribal knowledge

- id: inventory/reserve-only-writes
  description: >
    All mutations to inventory_units must
    go through Inventory::Reserve. Direct
    SQL or ActiveRecord updates skip the
    warehouse lock and can oversell during
    flash sales.

Why it’s powerful

This is a quirk of how this particular system works. Every pull request has to respect it, or a perfectly ordinary UPDATE can lose money. Checking that knowledge into version control means new engineers and coding agents are held to the same production constraint as the people who learned it the hard way.

Architecture / responsibility boundaries

- id: architecture/thin-controllers
  description: >
    Controllers should only authenticate,
    validate input, and orchestrate calls.
    Business rules and complex data
    transformations belong in domain
    services.

Why it’s powerful

Traditional linters can enforce dependency direction or file structure, but they cannot reliably decide whether a block of perfectly valid code is business logic. IntentLint can enforce architectural intent rather than syntax.

Safe database rollout patterns

- id: migrations/expand-and-contract
  description: >
    Changes to large production tables must
    be backwards-compatible with the currently
    deployed application. New required columns
    must be introduced using an expand-and-contract
    migration rather than added as immediately
    required.

Why it’s powerful

Whether a migration is operationally safe depends on what the migration is doing and how the application will behave during a rolling deploy. This catches dangerous changes that can look completely reasonable to a schema linter.

Business invariants that span implementation details

- id: billing/paid-feature-after-payment
  description: >
    A user must never receive access to a
    paid feature until payment has been
    successfully recorded. Changes to
    checkout, webhook, subscription, or
    entitlement code must preserve this
    invariant.

Why it’s powerful

The rule describes the behavior the system must preserve, not a particular function call or code pattern. Engineers can refactor the implementation freely while IntentLint checks that the underlying product invariant still holds.

In-progress work and refactors

- id: auth/rbac-not-legacy-acl
  description: >
    New authorization checks must use the
    RBAC system. Do not add calls to the
    legacy permissioning helpers
    (LegacyACL, can_access?, or
    Permission.for). Existing legacy checks
    may remain until the migration is done.

Why it’s powerful

A temporary rule can name the refactor and require new code to use RBAC instead of the old permissioning system. Traditional linters can ban a symbol, but they cannot tell whether a change is extending the legacy path or merely touching a file that still contains it. When the migration is done, delete the rule.

Enforce rules with dynamic exceptions

- id: i18n/no-direct-fr-de-edits
  description: >
    French and German translation YAML files
    must not be edited by hand. Translated
    strings come from the translation service.
    Removing a string entirely or moving a
    file is allowed. Changing translation
    values or adding new translated copy in
    these files is not.

Why it’s powerful

CODEOWNERS can require a review on every fr.yml or de.yml change, and a path linter can forbid those files entirely. Neither can tell a hand-edited French string from a legitimate key deletion or a file move. IntentLint evaluates what the change is doing, not only which path it touches.

Rules about data exposure

- id: product/no-shadow-ban-disclosure
  description: >
    In this application, users can be
    shadow-banned. Users who are
    shadow-banned should not be told that
    they are shadow-banned, and the API
    must never expose this information in
    its responses.

Why it’s powerful

Whether a response leaks a hidden status is a product invariant, not a syntax pattern. A traditional linter can ban a field name, but it cannot tell whether a new serializer, error message, or API shape would let a shadow-banned user discover that they are banned. IntentLint reads the rule and the change and flags accidental disclosure.

Install on GitHub Read the guide