v0.3.0 — now on Effect 4.1

If you can say it,
you can lint it.

lint-anything checks code, commits, and anything else a shell command can select against rules written in plain English. Cloudflare's Clef decision model judges every item as a yes-or-no question.

.lint-anything/rules/todo-has-issue.md
---
select: |
  rg --json -w 'TODO|FIXME' -g '*.ts' |
    jq -c 'select(.type == "match") | .data |
      {id: "\(.path.text):\(.line_number)", path: .path.text,
       lines: [.line_number, .line_number],
       text: "comment in \(.path.text):\n\(.lines.text)"}'
criteria:
  "true": "The comment has no tracking issue."
  "false": "The comment references a tracking issue, or isn't a TODO or FIXME."
---
Each question shows one comment. Decide whether it breaks the rule below.

Rule: every `TODO` or `FIXME` comment must reference a tracking issue, written as `#123`, `ABC-123`, or a full issue URL.
terminal ~/shop
$ bunx lint-anything
violation     0.95   src/billing.ts:3-3  todo-has-issue  The comment has no tracking issue.
violation     0.97   src/currency.ts:3-3  todo-has-issue  The comment has no tracking issue.
violation     0.94   src/report.ts:6-6  todo-has-issue  The comment has no tracking issue.
4 checks: 3 violation, 0 review, 0 inconclusive, 1 pass

Real policies, real output from Clef. The background is a field of items waiting to be judged: hover to inspect one, click to judge a few.

"Names reveal intent""No flag parameters""Comments explain why""Errors keep their context""Decisions stay separate from I/O""Entry points stay thin""One level of abstraction per function""Magic values get names""Tests check behavior, not implementation""Modules are cohesive""Every TODO has an issue""Commit messages say why"
rg --jsonast-grep scangit logjq -cgh pr listgit diffcurl -ssqlite3 -jsonfind . -namebun select.tsgit blamekubectl get -o json
Semantic rules

Lint the rules a parser can't see

Naming, design, readability, architecture: rules that take judgment. Write them the way you'd explain them to a reviewer.

Rules in plain English

A policy's body is sent to Clef exactly as written. No DSL, no plugin API, no AST visitors: the rule, plus whatever framing and examples help.

Select anything

A selector is a shell command that prints JSON lines. Comments from rg, functions from ast-grep, commits from git log, records from an API: if a command can print it, Clef can judge it.

A decision model, not a chat

Clef answers each item with a violation probability. At or below 0.5 it passes, at 0.65 or above it's a violation, and the uncertain middle goes to review.

Made for coding agents

Agents write the policies, run the linter, and fix what it finds. The package ships a skill that covers all three, and a script that scores a policy against labeled code.

Cached and cheap

Answers are cached in SQLite, keyed by the question. A re-run with nothing changed sends nothing; editing one policy re-asks only that policy.

Measured, not guessed

The CLI's design comes from experiments on 623 labeled files, 462 of them copied from 149 open-source projects. Tune your own policies the same way.

One Markdown file

A policy asks one question of every item

The frontmatter says what to judge and what a violation is. The body is the rule. The CLI adds nothing to what a policy asks.

.lint-anything/rules/todo-has-issue.md
---
select: |
  rg --json -w 'TODO|FIXME' -g '*.ts' |
    jq -c 'select(.type == "match") | .data |
      {id: "\(.path.text):\(.line_number)", path: .path.text,
       lines: [.line_number, .line_number],
       text: "comment in \(.path.text):\n\(.lines.text)"}'
criteria:
  "true": "The comment has no tracking issue."
  "false": "The comment references a tracking issue, or isn't a TODO or FIXME."
---
Each question shows one comment. Decide whether it breaks the rule below.

Rule: every `TODO` or `FIXME` comment must reference a tracking issue,
written as `#123`, `ABC-123`, or a full issue URL.
  1. select

    What gets judged

    A shell command, run once per lint run from the project root. It prints one JSON item per line: an id, the text Clef reads, and optionally a path and lines. Each item is one yes-or-no question.

  2. criteria

    What yes and no mean

    "true" describes an item that breaks the rule, "false" one that follows it. The true criterion is also the reason printed on every finding.

  3. body

    The rule itself

    The text of every request, sent exactly as written. Say what to decide, then state the rule. Add framing or examples if they help Clef judge it.

Select anything

If a command can print it, Clef can judge it

Selectors cut out exactly what a rule is about, and put the context it needs into each item. Every finding below came from running these policies.

Comments

rg + jq

Each TODO or FIXME line, with its file.

select: |
  rg --json -w 'TODO|FIXME' -g '*.ts' |
    jq -c 'select(.type == "match") | .data |
      {id: "\(.path.text):\(.line_number)", path: .path.text,
       lines: [.line_number, .line_number],
       text: "comment in \(.path.text):\n\(.lines.text)"}'
violation 0.95 src/billing.ts:3-3 todo-has-issue The comment has no tracking issue.

Functions

ast-grep

Every function declaration, cut out by syntax, not by line.

select: |
  ast-grep scan --json=stream --inline-rules \
    '{id: fn, language: TypeScript, rule: {kind: function_declaration}}' src |
    jq -c '{id: "\(.file):\(.range.start.line + 1)", path: .file,
      lines: [.range.start.line + 1, .range.end.line + 1],
      text: "function in \(.file):\n\(.text)"}'
violation 0.96 src/report.ts:7-14 no-flag-parameters The function takes a boolean that switches it between different jobs.

Commits

git log

History has no file or line: an item needs only an id.

select: |
  git log -n 20 --format=%h | while read -r hash; do
    git log -1 --format=%B "$hash" |
      jq -Rsc --arg id "$hash" '{id: $id, text: ("commit message:\n" + .)}'
  done
violation 0.97 76b76a5 commits-say-why The commit message doesn't say why.

Error messages

rg -U

Multi-line matches keep their full line range.

select: |
  rg --json -U 'new Error\([^)]*\)' -g '*.ts' |
    jq -c 'select(.type == "match") | .data |
      (.lines.text | rtrimstr("\n") | split("\n") | length) as $n |
      {id: "\(.path.text):\(.line_number)", path: .path.text,
       lines: [.line_number, .line_number + $n - 1],
       text: "error in \(.path.text):\n\(.lines.text)"}'
violation 0.98 src/report.ts:18-18 errors-say-what-to-do The message doesn't say what went wrong or what to do about it.

Counting

limit

Judge each item, then report once per file or per run when more than max items violate.

select: |
  ast-grep scan --json=stream --inline-rules \
    '{id: fn, language: TypeScript, rule: {kind: function_declaration}}' src |
    jq -c '…'
limit: { max: 1, per: run }
violation - flag-budget 3 of 6 items violate the rule (at most 1 allowed): src/billing.ts:4-12, src/currency.ts:12-17, src/report.ts:7-14

Third-party services

gh

Selectors run without any CLOUDFLARE_ variable, so a policy's shell never sees your API token.

select: |
  gh pr list --json number,title,body |
    jq -c '.[] | {id: "#\(.number)",
      text: "pull request #\(.number): \(.title)\n\n\(.body)"}'
criteria:
  "true": "The description doesn't say how the change was tested."
  "false": "The description says how the change was tested."
How it works

From shell output to verdicts

Up to 8 policies run at once, with at most 8 requests in flight across the run. Findings print in policy order.

  1. 01

    Select

    Each policy's selector runs once and prints its items. Policies with the same command share one run.

  2. 02

    Pack

    Items are sorted by path, line, and id, then packed up to 64 questions and 64,000 bytes per request.

  3. 03

    Cache

    Questions answered in the last 30 days come from SQLite. Only new or changed ones are sent.

  4. 04

    Judge

    Clef returns a violation probability per question. Transient failures are retried with backoff.

  5. 05

    Decide

    Probabilities become verdicts, and limit policies count them per file or per run.

Every item gets a probability. Two cut-offs make it a verdict.

The 19 items from the policies above, as Clef answered them. Hover or tap a point to see which item it is.

 

Output an agent can act on

One line per finding that didn't pass: verdict, probability, location, policy, and reason. Then a count per verdict. Only a violation fails the run.

ExitMeaning
0No violations
1At least one violation
2A policy, selector, configuration, or Clef API error stopped the run
130Interrupted; answers already received stay cached

This run came after one new file and one new commit: 5 new questions were sent and the other 14 answers came from the cache. --debug prints the cost table to stderr.

~/shop
$ bunx lint-anything --debug
violation     0.82   49554c5  commits-say-why  The commit message doesn't say why.
violation     0.95   4c19fe8  commits-say-why  The commit message doesn't say why.
violation     0.97   76b76a5  commits-say-why  The commit message doesn't say why.
violation     0.96   a8e0ffc  commits-say-why  The commit message doesn't say why.
violation     0.91   src/billing.ts:6-6  errors-say-what-to-do  The message doesn't say what went wrong or what to do about it.
violation     0.87   src/currency.ts:7-7  errors-say-what-to-do  The message doesn't say what went wrong or what to do about it.
violation     0.98   src/report.ts:18-18  errors-say-what-to-do  The message doesn't say what went wrong or what to do about it.
violation     0.91   src/billing.ts:4-12  no-flag-parameters  The function takes a boolean that switches it between different jobs.
violation     0.76   src/currency.ts:12-17  no-flag-parameters  The function takes a boolean that switches it between different jobs.
violation     0.96   src/report.ts:7-14  no-flag-parameters  The function takes a boolean that switches it between different jobs.
violation     0.95   src/billing.ts:3-3  todo-has-issue  The comment has no tracking issue.
violation     0.97   src/currency.ts:3-3  todo-has-issue  The comment has no tracking issue.
violation     0.94   src/report.ts:6-6  todo-has-issue  The comment has no tracking issue.
19 checks: 13 violation, 0 review, 0 inconclusive, 6 pass
policy                 requests  cached  input tokens  cost (USD)
commits-say-why               1       4           189     $0.0000
errors-say-what-to-do         1       3           209     $0.0001
no-flag-parameters            1       4           409     $0.0001
todo-has-issue                1       3           215     $0.0001
total                         4      14          1022     $0.0002
Evals

Measured on code nobody wrote for it

17 semantic rules on the test split: real files, copied verbatim from open-source projects and labeled by two model families.

0.930 file AUROC

How well a file's score separates violating files from clean ones.

85% of violations surfaced

As violation or review; 69% as violation outright.

13% false alarms

Clean cases that didn't pass. A guide more than a gate: read findings critically.

$13.98 per 1,000 files

At Clef's input-token price, for all 17 rules together.

623labeled files
10,623labeled cases
149open-source projects
2model families labeling

Rule-specific selectors beat judging files in 1,000-byte excerpts: +0.016 AUROC on the test split, and 70% → 80% of violations found at the same 10% false-alarm rate, for less money. Labels come from Claude and GPT, with disagreements adjudicated, and ambiguous cases stay ambiguous. Every hypothesis, method, and result is in the research notebook.

Agent skill

Agents write the rules.
Agents fix the findings.

The package ships a skill that teaches a coding agent to run lint-anything, act on what it finds, and write and tune policies.

  • No arguments, one flag. Every run checks every policy, so there is no wrong way to call it.
  • Fix, then re-run. Only changed items are asked again, so the loop stays fast and cheap.
  • No ignore comments. No baselines or suppressions either. A policy that keeps raising false alarms is the thing to fix.
  • Tune against labels. score.js reports reach, AUROC with 95% intervals, caught, surfaced, and false alarms, and compares a candidate policy with the current one.
# copy the skill into your agent's skills directory
cp -R node_modules/@andrueandersoncs/lint-anything/skills/lint-anything \
  .claude/skills/

# score a policy against .lint-anything/labels/todo-has-issue.jsonl
bun .claude/skills/lint-anything/scripts/score.js todo-has-issue

# and a candidate against it, with paired intervals
bun .claude/skills/lint-anything/scripts/score.js todo-has-issue candidate.md
Get started

Quick start

One package, one directory of policies, no configuration file.

  1. 1

    Install

    Needs Bun 1.2.23 or later, plus whatever your selectors call. rg, jq, and ast-grep are common; none are bundled.

    bun add -d @andrueandersoncs/lint-anything
  2. 2

    Connect Workers AI

    Your Cloudflare account ID and an API token. Bun loads .env and .env.local on its own.

    echo 'CLOUDFLARE_ACCOUNT_ID=...' >> .env
    echo 'CLOUDFLARE_AUTH_TOKEN=...' >> .env
    echo '.lint-anything/cache' >> .gitignore
  3. 3

    Write a policy, or ask your agent to

    Any Markdown file under .lint-anything/rules. Its name is its path there, without .md.

    ---
    select: |
      rg --json -w 'TODO|FIXME' -g '*.ts' |
        jq -c 'select(.type == "match") | .data |
          {id: "\(.path.text):\(.line_number)", path: .path.text,
           lines: [.line_number, .line_number],
           text: "comment in \(.path.text):\n\(.lines.text)"}'
    criteria:
      "true": "The comment has no tracking issue."
      "false": "The comment references a tracking issue, or isn't a TODO or FIXME."
    ---
    Each question shows one comment. Decide whether it breaks the rule below.
    
    Rule: every `TODO` or `FIXME` comment must reference a tracking issue.
  4. 4

    Run it

    From the project root, in CI or in your agent's loop.

    bunx lint-anything           # every policy, every time
    bunx lint-anything --debug   # plus requests, cache hits, and cost on stderr
  5. 5

    Or compose it yourself

    The stages a run composes are exported as an Effect library. Install effect 4.1 or later to use it.

    import { BunServices } from "@effect/platform-bun"
    import { Effect, Layer } from "effect"
    import {
      Clef, decide, defaultThreshold, judge, loadPolicies, select
    } from "@andrueandersoncs/lint-anything"
    
    const findings = await Effect.runPromise(Effect.gen(function*() {
      const [policy] = yield* loadPolicies(".lint-anything/rules")
      const items = yield* select(policy!)
      return decide(policy!, items, yield* judge(policy!, items), defaultThreshold)
    }).pipe(Effect.provide(Layer.mergeAll(Clef.layer(".lint-anything/cache"), BunServices.layer))))

Write down the rule you keep repeating in review.

Then let your agent enforce it on every run.