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.
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.
---
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.
$ 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.
Naming, design, readability, architecture: rules that take judgment. Write them the way you'd explain them to a reviewer.
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.
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.
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.
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.
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.
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.
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.
---
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.
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.
"true" describes an item that breaks the rule,
"false" one that follows it. The true criterion is
also the reason printed on every finding.
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.
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.
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.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.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.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.
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
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."
Up to 8 policies run at once, with at most 8 requests in flight across the run. Findings print in policy order.
Each policy's selector runs once and prints its items. Policies with the same command share one run.
Items are sorted by path, line, and id, then packed up to 64 questions and 64,000 bytes per request.
Questions answered in the last 30 days come from SQLite. Only new or changed ones are sent.
Clef returns a violation probability per question. Transient failures are retried with backoff.
Probabilities become verdicts, and limit policies
count them per file or per run.
The 19 items from the policies above, as Clef answered them. Hover or tap a point to see which item it is.
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.
| Exit | Meaning |
|---|---|
0 | No violations |
1 | At least one violation |
2 | A policy, selector, configuration, or Clef API error stopped the run |
130 | Interrupted; 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.
$ 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
17 semantic rules on the test split: real files, copied verbatim from open-source projects and labeled by two model families.
How well a file's score separates violating files from clean ones.
As violation or review; 69% as violation outright.
Clean cases that didn't pass. A guide more than a gate: read findings critically.
At Clef's input-token price, for all 17 rules together.
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.
The package ships a skill that teaches a coding agent to run lint-anything, act on what it finds, and write and tune policies.
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
One package, one directory of policies, no configuration file.
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
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
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.
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
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))))
Then let your agent enforce it on every run.